Using yt-dlp from Python: the embedded API, options, hooks and running it in a server
By Tunelio teamPublished
yt-dlp is a Python package, so you can import it instead of shelling out: yt_dlp.YoutubeDL(opts).extract_info(url) gives you the metadata and, if you want, the file. This guide covers the options dict, getting formats without downloading, progress hooks, error handling, keeping an async web server responsive, and the Docker image you need — plus what it costs per video in CPU when you run it for other people.
Install
python3 -m pip install -U "yt-dlp[default]"
# plus ffmpeg on the machine (merging / audio) and a JS runtime such as Deno (YouTube challenges)Minimal download
import yt_dlp
opts = {
"format": "bv*+ba/b", # best video + best audio, merged (needs ffmpeg)
"merge_output_format": "mp4",
"outtmpl": "downloads/%(title)s.%(ext)s",
"noplaylist": True,
"quiet": True,
}
with yt_dlp.YoutubeDL(opts) as ydl:
ydl.download(["https://youtu.be/dQw4w9WgXcQ"])Every command-line flag has a dictionary key; the mapping is documented in yt_dlp/YoutubeDL.py (the class docstring) and yt_dlp/options.py. When in doubt, run the CLI with the flags you want and --print-json, then copy the option names.
Metadata and formats without downloading
import yt_dlp
with yt_dlp.YoutubeDL({"quiet": True, "skip_download": True}) as ydl:
info = ydl.extract_info("https://youtu.be/dQw4w9WgXcQ", download=False)
print(info["title"], info["duration"])
for f in info["formats"]:
if f.get("vcodec") != "none" and f.get("acodec") != "none": # progressive (video+audio in one file)
print(f["format_id"], f.get("height"), f.get("ext"), f.get("filesize_approx"))
best_audio = max((f for f in info["formats"] if f.get("vcodec") == "none"), key=lambda f: f.get("abr") or 0)
print("audio:", best_audio["format_id"], best_audio["url"][:60], "...")extract_info returns the same JSON the CLI prints with -j: formats with direct googlevideo URLs, thumbnails, subtitles, chapters. The URLs are signed for the IP and client that requested them and expire after roughly six hours — hand them to ffmpeg or a downloader on the same machine, not to your users.
Progress hooks and logging
def on_progress(d):
if d["status"] == "downloading":
print(d.get("_percent_str"), d.get("_speed_str"), end="\r")
elif d["status"] == "finished":
print("\ndone, post-processing:", d["filename"])
class QuietLogger:
def debug(self, msg): pass
def warning(self, msg): print("WARN", msg)
def error(self, msg): print("ERR", msg)
opts = {"progress_hooks": [on_progress], "logger": QuietLogger(), "format": "bv*+ba/b"}Error handling
from yt_dlp.utils import DownloadError, ExtractorError
try:
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(url, download=False)
except DownloadError as e:
msg = str(e)
if "Sign in to confirm" in msg: # bot wall → IP/cookies/PO token, see the bot-check guide
...
elif "HTTP Error 403" in msg: # see the 403 guide
...
elif "Private video" in msg or "Video unavailable" in msg:
...
raiseyt-dlp wraps almost everything in DownloadError with the original message inside; match on the message text. Set "ignoreerrors": True for playlists so one dead item does not abort the run (entries then come back as None).
Cookies, proxies and other production options
opts = {
"cookiefile": "/secrets/cookies.txt", # Netscape format; spare account only
"proxy": "socks5h://user:pass@exit.example:1080",
"source_address": "0.0.0.0", # force IPv4 (the -4 flag)
"extractor_args": {"youtube": {"player_client": ["tv", "web"]}},
"sleep_interval_requests": 1,
"retries": 3,
"socket_timeout": 30,
}The same rules as on the command line apply: cookies from a private window of a spare account, residential exits rather than datacenter ones, IPv4 unless you know the IPv6 range is clean. The cookies, proxy and 403 guides explain each.
Inside an async server (FastAPI, aiohttp, Telegram bots)
import asyncio
from fastapi import FastAPI
app = FastAPI()
_sem = asyncio.Semaphore(4) # cap concurrent extractions — each one is ~1 s of CPU
def _extract(url: str) -> dict:
with yt_dlp.YoutubeDL({"quiet": True, "skip_download": True}) as ydl:
return ydl.extract_info(url, download=False)
@app.get("/info")
async def info(url: str):
async with _sem:
data = await asyncio.to_thread(_extract, url) # never call yt-dlp on the event loop
return {"title": data["title"], "duration": data["duration"]}yt-dlp is synchronous and CPU-heavy: on our servers a single YouTube extraction costs about 1.2–1.3 seconds of CPU, most of it running the player JavaScript. Called directly inside an async handler it freezes every other request; run it in a thread or process pool and cap concurrency, or your service will fall over at a few requests per second.
Docker
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg curl unzip ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh
RUN pip install --no-cache-dir "yt-dlp[default]" fastapi uvicorn
COPY app.py /app/app.py
USER 1000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--app-dir", "/app"]Three runtime dependencies: yt-dlp, ffmpeg and a JavaScript runtime. Rebuild the image at least monthly (see the update guide) — a container with a three-month-old yt-dlp will fail on most videos. Run as a non-root user with its own cache directory.
Cost and reliability at scale
- CPU: ~1.2–1.3 s per extraction on a modern core, paid on every request (the player JS is re-evaluated). Ten requests per second needs a dozen cores just for yt-dlp.
- IP reputation: a server that extracts hundreds of videos a day from one address will hit the bot wall; you end up running proxies and PO-token services.
- Breakage: every YouTube change is a redeploy; the update guide explains why.
- Bandwidth: media you relay to users flows through your server twice.
Those are the reasons hosted APIs exist. Tunelio (our own service) does the extraction and returns a download URL per request, so your Python stays a single HTTP call and no ffmpeg, runtime or proxy lives in your container. For a script on your laptop, the embedded API above is the right tool. New accounts get 100 free credits.
Sources
- yt-dlp README — “Embedding yt-dlp” section and the options reference in yt_dlp/YoutubeDL.py.
- yt-dlp wiki — Extractor args (player_client) and PO token guide.
- Our own measurements (Tunelio extraction fleet, 2026): CPU per extraction, bot-wall rates on datacenter IPs.
Frequently asked questions
Should I call the yt-dlp CLI with subprocess or import it?
Importing is cleaner and gives structured data; subprocess isolates crashes and memory. Either way, run it off the event loop and cap concurrency.
Can I get the direct video URL without downloading?
Yes: extract_info(url, download=False) returns every format with its URL. They are tied to your IP/client and expire in about six hours, so use them server-side immediately.
Why does my FastAPI app freeze while yt-dlp runs?
yt-dlp is blocking and CPU-heavy. Wrap it in asyncio.to_thread (or a process pool) and limit parallel extractions with a semaphore.
Which option keys map to which flags?
Most flags become snake_case keys (--merge-output-format → merge_output_format). The authoritative list is the YoutubeDL class docstring in yt_dlp/YoutubeDL.py.