MoviePy 2 Tutorial: Edit Videos with Python (Trim, Text, Merge, Audio) 2026
Cutting the same intro off fifty screen recordings, stamping a title on every product clip or stitching highlights into a short reel are perfect jobs for a script. MoviePy is the most popular pure-Python library for that kind of video editing, but most tutorials online still show the old 1.x API, so code that starts with from moviepy.editor import * now fails straight away. In this guide we use the current MoviePy 2 API end to end: generate a sample clip, trim and resize it, add a text title with fades, merge parts with speed and volume changes, and export MP4, MP3 and GIF files. Everything was tested on Python 3.13.5 with MoviePy 2.2.1, NumPy 2.5.4, Pillow 11.3.0 and imageio-ffmpeg 0.8.1, and every output block below is the real output of the scripts shown.
Why MoviePy 2 in 2026?
- Simple imports: everything comes from
moviepydirectly; the heavymoviepy.editormodule is gone. - Immutable, chainable edits: methods such as
with_duration(),with_position(),subclipped()andresized()return a new clip and leave the original untouched. - Effects are classes:
clip.with_effects([vfx.FadeIn(0.5)])replaces the oldclip.fx(...)calls and makes effects easy to combine. - Fewer system dependencies: text rendering uses Pillow instead of ImageMagick, and an FFmpeg binary is installed for you through
imageio-ffmpeg. - Great for automation: batch-edit recordings, generate social clips or build narrated videos from scripts and data.
Install
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install moviepy==2.2.1
# pulls numpy, pillow, imageio, imageio-ffmpeg (bundled FFmpeg), proglog
You do not need to install FFmpeg or ImageMagick separately. For TextClip you only need a TrueType font file; the examples use DejaVu Sans, which ships with most Linux distributions. On Windows use a path such as C:\Windows\Fonts\arial.ttf.
Step 1: Create a sample video (no download needed)
So the tutorial works anywhere, we build a 10-second, 640x360 test video in code. VideoClip takes a function that returns a NumPy RGB frame for time t, and AudioClip does the same for sound samples (a 440 Hz tone).
"""Create a 10-second sample video with a tone, using only MoviePy + NumPy."""
import numpy as np
from moviepy import VideoClip, AudioClip
W, H, FPS, DUR = 640, 360, 24, 10
def frame(t):
x = np.linspace(0, 1, W)
y = np.linspace(0, 1, H)
red = np.tile((x + t / DUR) % 1.0, (H, 1)) # moving stripes
green = np.tile(y[:, None], (1, W)) # vertical gradient
blue = np.full((H, W), 0.5 + 0.5 * np.sin(t)) # pulsing blue
return (np.dstack([red, green, blue]) * 255).astype("uint8")
def tone(t):
t = np.asarray(t)
wave = 0.2 * np.sin(2 * np.pi * 440 * t)
return np.column_stack([wave, wave]) if wave.ndim else [wave, wave]
video = VideoClip(frame, duration=DUR).with_fps(FPS)
audio = AudioClip(tone, duration=DUR, fps=44100)
video = video.with_audio(audio)
video.write_videofile("sample.mp4", codec="libx264", audio_codec="aac", logger=None)
print("wrote sample.mp4", video.size, f"{video.duration}s", f"{video.fps}fps")
$ python make_sample.py
wrote sample.mp4 (640, 360) 10s 24fps
Swap sample.mp4 for your own recording in the next steps; nothing else changes.
Step 2: Trim, resize and crop a clip
from moviepy import VideoFileClip
clip = VideoFileClip("sample.mp4")
print("source:", clip.size, f"{clip.duration:.2f}s", f"{clip.fps:.0f}fps", "audio:", clip.audio is not None)
short = clip.subclipped(2, 6) # v1: subclip()
small = short.resized(width=480) # v1: resize()
square = small.cropped(x_center=240, y_center=135, width=270, height=270) # v1: crop()
small.write_videofile("trimmed.mp4", codec="libx264", audio_codec="aac", logger=None)
square.without_audio().write_videofile("square.mp4", codec="libx264", logger=None)
for name in ("trimmed.mp4", "square.mp4"):
c = VideoFileClip(name)
print(f"{name}: {c.size} {c.duration:.2f}s audio={c.audio is not None}")
c.close()
clip.close()
$ python trim_resize.py
source: [640, 360] 10.00s 24fps audio: True
trimmed.mp4: [480, 270] 4.00s audio=True
square.mp4: [270, 270] 4.00s audio=False
subclipped(2, 6) keeps seconds 2 to 6, resized(width=480) scales while keeping the 16:9 ratio (480x270), and cropped() cuts a centred 270x270 square, which is handy for social feeds. without_audio() drops the soundtrack on export. Passing logger=None hides the progress bar; leave it out to see one while long videos render.
Step 3: Add a text title with fades
from moviepy import VideoFileClip, TextClip, CompositeVideoClip, vfx
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" # v2 needs a font file path
base = VideoFileClip("trimmed.mp4")
title = (
TextClip(font=FONT, text="MoviePy 2 Demo", font_size=40, color="white",
bg_color="black", margin=(16, 8))
.with_position(("center", "top"))
.with_start(0.5)
.with_duration(3)
.with_effects([vfx.CrossFadeIn(0.5), vfx.CrossFadeOut(0.5)])
)
final = CompositeVideoClip([base, title]).with_effects([vfx.FadeIn(0.4), vfx.FadeOut(0.4)])
final.write_videofile("titled.mp4", codec="libx264", audio_codec="aac", logger=None)
final.save_frame("titled_frame.png", t=2)
check = VideoFileClip("titled.mp4")
print("titled.mp4:", check.size, f"{check.duration:.2f}s")
print("text layer:", title.size, "starts", title.start, "ends", title.end)
print("frame at 0s mean brightness:", round(check.get_frame(0).mean(), 1))
print("frame at 2s mean brightness:", round(check.get_frame(2).mean(), 1))
check.close(); base.close()
$ python title_overlay.py
titled.mp4: [480, 270] 4.00s
text layer: (408, 55) starts 0.5 ends 3.5
frame at 0s mean brightness: 0.0
frame at 2s mean brightness: 90.9
TextClip now needs font= pointing at a font file, and font_size replaces the old fontsize. The title layer runs from 0.5 s to 3.5 s on top of the base clip inside CompositeVideoClip, fading in and out with CrossFadeIn/CrossFadeOut. The whole video also fades from black: the first frame has mean brightness 0.0, while the frame at 2 s is fully visible (90.9). save_frame() writes a PNG still you can use as a thumbnail.
Step 4: Merge clips, change speed and volume, export MP3 and GIF
from moviepy import VideoFileClip, concatenate_videoclips, afx, vfx
clip = VideoFileClip("sample.mp4")
parts = [clip.subclipped(0, 2), clip.subclipped(4, 6), clip.subclipped(8, 10)]
fast = parts[1].with_effects([vfx.MultiplySpeed(2)]) # 2s -> 1s
quiet = parts[2].with_effects([afx.MultiplyVolume(0.3)]) # 30% volume
reel = concatenate_videoclips([parts[0], fast, quiet], method="compose")
reel.write_videofile("reel.mp4", codec="libx264", audio_codec="aac", logger=None)
reel.audio.write_audiofile("reel_audio.mp3", logger=None)
reel.resized(width=320).with_fps(10).write_gif("reel.gif", logger=None)
out = VideoFileClip("reel.mp4")
print("parts:", [round(p.duration, 2) for p in (parts[0], fast, quiet)])
print("reel.mp4:", out.size, f"{out.duration:.2f}s")
print("loudness start vs end:",
round(abs(out.audio.subclipped(0.5, 1).to_soundarray()).max(), 3),
round(abs(out.audio.subclipped(3.5, 4).to_soundarray()).max(), 3))
import os
for f in ("reel.mp4", "reel_audio.mp3", "reel.gif"):
print(f"{f}: {os.path.getsize(f) // 1024} KB")
out.close(); clip.close()
$ python concat_audio.py
parts: [2, 1.0, 2]
reel.mp4: [640, 360] 5.00s
loudness start vs end: 0.206 0.061
reel.mp4: 172 KB
reel_audio.mp3: 79 KB
reel.gif: 623 KB
Three 2-second parts become one reel: the middle part plays at double speed (MultiplySpeed(2) turns 2 s into 1 s), and the last part drops to 30% volume with afx.MultiplyVolume(0.3), which the peak loudness check confirms (0.206 near the start vs 0.061 near the end). concatenate_videoclips(..., method="compose") safely joins clips even if their sizes differ. Finally we export just the soundtrack with write_audiofile() and a small 320 px, 10 fps GIF with write_gif().
Step 5: Fix old MoviePy 1.x code
try:
from moviepy.editor import VideoFileClip # MoviePy 1.x style
except ModuleNotFoundError as err:
print("ModuleNotFoundError:", err)
from importlib.metadata import version
from moviepy import VideoFileClip
clip = VideoFileClip("sample.mp4")
print("moviepy", version("moviepy"))
print("has set_duration:", hasattr(clip, "set_duration"), "| has with_duration:", hasattr(clip, "with_duration"))
print("has subclip:", hasattr(clip, "subclip"), "| has subclipped:", hasattr(clip, "subclipped"))
print("has resize:", hasattr(clip, "resize"), "| has resized:", hasattr(clip, "resized"))
clip.close()
$ python v1_import.py
ModuleNotFoundError: No module named 'moviepy.editor'
moviepy 2.2.1
has set_duration: False | has with_duration: True
has subclip: False | has subclipped: True
has resize: False | has resized: True
If you see ModuleNotFoundError: No module named 'moviepy.editor', you are running 1.x code on MoviePy 2. Use this quick mapping:
| MoviePy 1.x | MoviePy 2.x |
|---|---|
from moviepy.editor import * | from moviepy import * |
clip.subclip(a, b) | clip.subclipped(a, b) |
clip.set_duration(), set_position(), set_start(), set_audio() | with_duration(), with_position(), with_start(), with_audio() |
clip.resize(), crop(), rotate() | resized(), cropped(), rotated() |
clip.fx(vfx.fadein, 1) | clip.with_effects([vfx.FadeIn(1)]) |
clip.volumex(0.5) | clip.with_effects([afx.MultiplyVolume(0.5)]) |
TextClip("Hi", fontsize=40) (ImageMagick) | TextClip(font="path.ttf", text="Hi", font_size=40) (Pillow) |
Common errors and fixes
AttributeError: ... has no attribute 'set_position': renameset_*towith_*. These methods return a new clip, so assign the result:clip = clip.with_position("center").- TextClip font errors: pass a real
.ttf/.otfpath tofont=; font family names such as"Arial"are not looked up for you. - No sound in the browser or on a phone: export with
codec="libx264", audio_codec="aac"for MP4 so players recognise both streams. - Files stay locked on Windows: call
clip.close()(or usewith VideoFileClip(...) as clip:) once you are done so the FFmpeg reader is released. - Slow renders: lower the output size with
resized(), passpreset="ultrafast"while testing andthreads=4for the final export. - Stale tutorials: check your version with
importlib.metadata.version("moviepy"); if you must keep 1.x code temporarily, pinmoviepy==1.0.3in that project only.
Where to use this next
- Create thumbnails and watermark logos for your clips with Pillow: Resize, Crop and Watermark Images in Python.
- Generate an offline voiceover and attach it with
with_audio()using Text to Speech in Python with Piper TTS. - Transcribe the finished video into subtitles with Speech to Text in Python with faster-whisper.
Conclusion
MoviePy 2 turns everyday video edits into a few readable lines: load a clip, chain subclipped(), resized() and with_* calls, add TextClip titles and effects with with_effects(), then join and export with concatenate_videoclips() and write_videofile(). Once you know the new names, old 1.x snippets are easy to upgrade, and batch video automation becomes just another Python script.