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
| Method | Install | Best for |
|---|---|---|
| moviepy | pip install moviepy | Simple scripts, Pythonic API, video+audio manipulation |
| ffmpeg-python | pip install ffmpeg-python | Direct FFmpeg control, complex filters, minimal overhead |
| ChangeThisFile API | None | No 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
| Approach | Best for | Tradeoff |
|---|---|---|
| moviepy | Simple scripts, video+audio manipulation combined | Heavier install; slower startup than ffmpeg-python for simple extraction |
| ffmpeg-python | Pipelines with filters, trim/split, multiple output streams | Thin wrapper — FFmpeg errors can be cryptic; probe manually for audio |
| ChangeThisFile API | No local FFmpeg, serverless, low-volume one-off conversions | Network 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: passbitrate="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.contentwithresponse.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.