MoviePy 2 Tutorial: Edit Videos with Python (Trim, Text, Merge, Audio) 2026

Automation Nov 10, 2026

Watch on YouTube

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 moviepy directly; the heavy moviepy.editor module is gone.
  • Immutable, chainable edits: methods such as with_duration(), with_position(), subclipped() and resized() return a new clip and leave the original untouched.
  • Effects are classes: clip.with_effects([vfx.FadeIn(0.5)]) replaces the old clip.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.xMoviePy 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': rename set_* to with_*. These methods return a new clip, so assign the result: clip = clip.with_position("center").
  • TextClip font errors: pass a real .ttf/.otf path to font=; 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 use with VideoFileClip(...) as clip:) once you are done so the FFmpeg reader is released.
  • Slow renders: lower the output size with resized(), pass preset="ultrafast" while testing and threads=4 for the final export.
  • Stale tutorials: check your version with importlib.metadata.version("moviepy"); if you must keep 1.x code temporarily, pin moviepy==1.0.3 in that project only.

Where to use this next

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.

Share:
Last updated: November 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices