MP4-to-MP3 in Python requires FFmpeg under the hood — there's no pure-Python audio transcoder with equivalent codec support. moviepy wraps FFmpeg with a clean Python API ideal for scripts and one-off conversions. ffmpeg-python provides a thin, composable FFmpeg wrapper for pipelines where you need explicit control over streams and filters. The ChangeThisFile API is the right call when you want no local FFmpeg installation.

TL;DR

MethodInstallBest for
moviepypip install moviepySimple scripts, Pythonic API, video+audio manipulation
ffmpeg-pythonpip install ffmpeg-pythonDirect FFmpeg control, complex filters, minimal overhead
ChangeThisFile APINoneNo FFmpeg installed, serverless, low-volume conversions

Method 1: moviepy (Pythonic API, FFmpeg backend)

moviepy wraps FFmpeg with a clean Python API. Good for scripts and notebooks — reads the video, extracts audio, and writes MP3 in a single chained call.

pip install moviepy
# FFmpeg must be installed:
# apt install ffmpeg        # Ubuntu/Debian
# brew install ffmpeg       # macOS
from moviepy import VideoFileClip
from pathlib import Path

def mp4_to_mp3(in_path: str, out_path: str, bitrate: str = "192k") -> None:
    with VideoFileClip(in_path) as clip:
        if clip.audio is None:
            raise ValueError(f"No audio stream in {in_path}")
        clip.audio.write_audiofile(
            out_path,
            bitrate=bitrate,
            logger=None,  # suppress progress output
        )

mp4_to_mp3("video.mp4", "audio.mp3")
print("Done")

# Batch conversion
def batch_mp4_to_mp3(src_dir: str, out_dir: str, bitrate: str = "192k") -> int:
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)
    count = 0
    for mp4 in Path(src_dir).glob("*.mp4"):
        mp4_to_mp3(str(mp4), str(out / mp4.with_suffix(".mp3").name), bitrate)
        count += 1
    return count

n = batch_mp4_to_mp3("./videos", "./audio")
print(f"Converted {n} files")

Bitrate guide: 128k = speech/podcast quality, 192k = good music quality, 320k = maximum MP3 quality. Use logger=None to suppress moviepy's default progress bar in scripts and automated pipelines.

Method 2: ffmpeg-python (direct FFmpeg control, composable)

ffmpeg-python builds FFmpeg command graphs as Python objects, then executes them. Thinner than moviepy and better for pipelines where you need to chain filters or handle multiple streams.

pip install ffmpeg-python
# FFmpeg must be installed:
# apt install ffmpeg
# brew install ffmpeg
import ffmpeg
from pathlib import Path

def mp4_to_mp3(in_path: str, out_path: str, audio_bitrate: str = "192k") -> None:
    (
        ffmpeg
        .input(in_path)
        .audio
        .output(out_path, audio_bitrate=audio_bitrate, acodec="mp3")
        .overwrite_output()
        .run(quiet=True)  # suppress FFmpeg console output
    )

mp4_to_mp3("video.mp4", "audio.mp3")

# Extract a clip (first 60 seconds)
def mp4_to_mp3_trim(
    in_path: str,
    out_path: str,
    start: float = 0,
    duration: float = 60,
    bitrate: str = "192k",
) -> None:
    (
        ffmpeg
        .input(in_path, ss=start, t=duration)
        .audio
        .output(out_path, audio_bitrate=bitrate, acodec="mp3")
        .overwrite_output()
        .run(quiet=True)
    )

mp4_to_mp3_trim("video.mp4", "intro.mp3", start=30, duration=60)

ffmpeg-python calls FFmpeg as a subprocess but avoids manual string building. .run(quiet=True) suppresses FFmpeg's stderr output — set quiet=False while debugging to see exact FFmpeg error messages.

Check for audio before converting: Some MP4 files (screen recordings, silent clips) have no audio stream. Detect this before attempting conversion:

def has_audio(path: str) -> bool:
    probe = ffmpeg.probe(path)
    return any(s["codec_type"] == "audio" for s in probe["streams"])

if has_audio("video.mp4"):
    mp4_to_mp3("video.mp4", "audio.mp3")
else:
    print("No audio stream found")

Method 3: ChangeThisFile API (requests, no FFmpeg)

POST the MP4 to the API, receive MP3. Source is auto-detected from the filename — pass only target=mp3. Free tier: 1,000 conversions/month, no card needed.

# Test with curl first
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key" \
  -F "file=@video.mp4" \
  -F "target=mp3" \
  --output audio.mp3
import requests
from pathlib import Path

API_KEY = "ctf_sk_your_key_here"

def mp4_to_mp3_api(in_path: str, out_path: str) -> None:
    with open(in_path, "rb") as f:
        response = requests.post(
            "https://changethisfile.com/v1/convert",
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": (Path(in_path).name, f, "video/mp4")},
            data={"target": "mp3"},
            timeout=300,  # large video files can take a while
        )
    response.raise_for_status()
    Path(out_path).write_bytes(response.content)

mp4_to_mp3_api("video.mp4", "audio.mp3")
print("Done")

Set a generous timeout — a 500MB MP4 uploading over a typical connection takes 30-60 seconds before transcoding begins. The API streams the response, so response.content waits for the full MP3 before returning.

When to use each

ApproachBest forTradeoff
moviepySimple scripts, video+audio manipulation combinedHeavier install; slower startup than ffmpeg-python for simple extraction
ffmpeg-pythonPipelines with filters, trim/split, multiple output streamsThin wrapper — FFmpeg errors can be cryptic; probe manually for audio
ChangeThisFile APINo local FFmpeg, serverless, low-volume one-off conversionsNetwork upload time; 25MB file limit on free tier

Production tips

  • Always check for an audio stream first. Some MP4 files (screen recordings, game captures) have no audio. Both moviepy and ffmpeg-python will raise cryptic errors on video-only MP4s. Use ffmpeg.probe() or moviepy's clip.audio is None check before converting.
  • 192kbps is the sweet spot for music. Most listeners can't distinguish 192kbps from 320kbps. 128kbps is fine for podcasts and voice. 320kbps is for archival or audiophile use only.
  • Use VBR for better quality-per-byte. ffmpeg-python: replace audio_bitrate with aq=2 (VBR quality 2 ≈ 190kbps). moviepy: pass bitrate="0" and add ffmpeg_params=["-q:a", "2"].
  • Parallelize batch conversions carefully. FFmpeg is CPU-bound. Running 8 concurrent conversions on a 4-core server will saturate CPU. Use concurrent.futures.ProcessPoolExecutor(max_workers=cpu_count()//2) for batch jobs.
  • For the API, use streaming reads for large files. Replace response.content with response.iter_content(chunk_size=8192) and write chunks to disk to avoid buffering an entire large MP3 in memory.

For scripts and notebooks, moviepy is the fastest path. For production pipelines with complex audio requirements, ffmpeg-python gives you direct control. The ChangeThisFile API is the right call when FFmpeg isn't available. Free tier: 1,000 conversions/month.