Introduction
Downloading a YouTube video for offline use — your own uploads, a lecture you’re presenting from a venue with bad Wi-Fi, a video you’re citing in research — is a legitimate need YouTube’s interface doesn’t serve. This tutorial builds a downloader with pytubefix (the maintained fork of pytube): resolution selection, audio-only extraction, and playlist support.
The honest caveat up front: YouTube’s terms of service restrict downloading except where explicitly permitted (YouTube Premium, your own content, Creative Commons). Use this for content you own or have rights to — the tool is the same either way, the responsibility is yours.
Features
- Stream selection — pick resolution from what actually exists.
- Audio-only mode — grab just the soundtrack.
- Playlist support — download every video in one run.
- Progress feedback — file size and completion per video.
- Safe filenames — sanitized titles, no filesystem surprises.
Prerequisites
- Python 3.8+ — from python.org.
- Dependencies:
pip install pytubefix
Step 1: Create the Script
Save as yt_download.py:
import sys
import re
from pytubefix import YouTube, Playlist
from pytubefix.cli import on_progress
def safe_name(title):
return re.sub(r'[\\/*?:"<>|]', "", title).strip()
def download_video(url, audio_only=False):
yt = YouTube(url, on_progress_callback=on_progress)
print(f"\n🎬 {yt.title} by {yt.author}")
if audio_only:
stream = yt.streams.filter(only_audio=True).order_by("abr").last()
suffix = ".m4a"
else:
stream = (yt.streams.filter(progressive=True, file_extension="mp4")
.order_by("resolution").desc().first())
suffix = ".mp4"
if stream is None:
print(" No suitable stream found.")
return
print(f" Downloading {stream.resolution or stream.abr} ({stream.filesize_mb:.0f}MB)...")
stream.download(filename=safe_name(yt.title) + suffix)
print(" ✅ Done")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(f"Usage: python {sys.argv[0]} <video-or-playlist-url> [--audio]")
url = sys.argv[1]
audio_only = "--audio" in sys.argv
if "playlist" in url:
pl = Playlist(url)
print(f"Playlist: {pl.title} ({len(pl.video_urls)} videos)")
for video_url in pl.video_urls:
try:
download_video(video_url, audio_only)
except Exception as e:
print(f" Skipped ({e})")
else:
download_video(url, audio_only)
Step 2: Run the Downloader
python yt_download.py "https://youtube.com/watch?v=..."
python yt_download.py "https://youtube.com/playlist?list=..." --audio
A progress bar tracks each download; playlist mode loops through every video, skipping failures without dying.
How It Works
pytubefix fetches the watch page, extracts the stream manifest, and exposes it as filterable objects. The key concept is that YouTube serves video and audio as separate streams for high resolutions (DASH); progressive=True filters the few streams that bundle both — which is why the video path caps at 720p. Higher resolutions require downloading video and audio separately and muxing them with ffmpeg — a deliberate step up in complexity.
The audio path sorts by abr (average bitrate) and takes .last() — counterintuitively, pytubefix orders audio streams so the lowest bitrate is first, making last() the quality pick. This asymmetry between video (.desc().first()) and audio (.last()) trips everyone once.
The playlist branch is the resilience lesson: each video downloads inside its own try/except, so one private or deleted video skips instead of killing a 50-video run — the same per-item error isolation the web scraper applies per page.
safe_name strips the seven characters filesystems reject — one regex that prevents a whole category of OSError surprises.
Common Errors & Fixes
HTTPError 404on a valid video — YouTube changed their internal structure; update pytubefix (pip install -U pytubefix), which is why we use the maintained fork over the abandoned original pytube.- Age-restricted videos fail — they require sign-in; pytubefix supports
use_oauth=Truefor your own account. - Downloads stall at 99% — usually network throttling by YouTube; retry, or add
on_completelogging to see the actual state. KeyError: 'streamData'— a transient extraction failure; retry once before assuming a library bug.
Key Concepts
- Stream manifests — video/audio as filterable, separate assets.
- Progressive vs DASH — why 720p is the progressive ceiling.
- Per-item error isolation — playlists and batches survive individual failures.
- Filename sanitization — one regex against filesystem surprises.
What to Try Next
- Add ffmpeg muxing for 1080p+: download best video + best audio, then merge.
- Add a Tkinter GUI — URL box and progress bar, using the paint app’s window patterns.
- Add subtitle download —
yt.captionsfor offline caption files. - Log downloads to CSV and chart them, habit-tracker style.
FAQ
Is downloading YouTube videos legal?
It depends on content rights and jurisdiction. Your own uploads, Creative Commons content, and Premium offline mode are safe harbors; mass-downloading others’ content generally violates YouTube’s terms.
Why pytubefix instead of pytube?
pytube is unmaintained and breaks whenever YouTube changes internals; pytubefix is the actively patched fork with identical API.
Can I download in 1080p or 4K?
Not progressively — high resolutions are DASH-only. Download best video + best audio separately and merge with ffmpeg (ffmpeg -i v.mp4 -i a.m4a -c copy out.mp4).