There are two practical ways to get YouTube transcripts in Python: install a free open-source library and fetch captions yourself, or call a managed API that handles blocking, metadata, and scale for you. This guide shows both, with copy-paste code, and is honest about where each one breaks down.
Option 1: the free youtube-transcript-api library
The most popular free option is the youtube-transcript-api package. It talks to YouTube's caption endpoints directly, so there's no API key and no cost.
pip install youtube-transcript-api
Fetch a transcript by video ID:
from youtube_transcript_api import YouTubeTranscriptApi
# Classic API (0.6.x and earlier) - returns a list of {text, start, duration}
transcript = YouTubeTranscriptApi.get_transcript("dQw4w9WgXcQ")
text = " ".join(chunk["text"] for chunk in transcript)
print(text)
Version 1.x changed to an instance-based API. If get_transcript is missing, you're on the new one:
from youtube_transcript_api import YouTubeTranscriptApi
ytt = YouTubeTranscriptApi()
fetched = ytt.fetch("dQw4w9WgXcQ")
text = " ".join(snippet.text for snippet in fetched)
You can also request specific languages, falling back in order:
YouTubeTranscriptApi.get_transcript("dQw4w9WgXcQ", languages=["en", "en-US", "es"])
That's genuinely all you need for a quick script. The catch is what the library doesn't do:
- It gets blocked from servers. YouTube tolerates requests from residential IPs but blocks datacenter ranges, so code that works on your laptop returns
IpBlocked/TooManyRequeststhe moment it runs on AWS, a CI runner, or a Lambda. You then have to wire up rotating residential proxies yourself. - No metadata. You get caption chunks - no title, channel, duration, or thumbnail.
- No discovery. You must already have the video IDs. There's no built-in way to list a channel, expand a playlist, or search.
- No batching or retries. Fetching hundreds of videos means writing your own concurrency, backoff, and caching - and absorbing the breakage every time YouTube changes its internals.
For a one-off script or a notebook, this is the right tool. For anything that runs unattended or at scale, the maintenance cost adds up fast.
Option 2: a managed API
A managed transcript API moves the proxy pool, retries, caching, and metadata to someone else's servers. You make a normal HTTP request and get clean JSON back. The examples below use TranscriptFetch and the httpx client (requests works the same way).
pip install httpx
Set your key once and fetch a single transcript:
import os
import httpx
KEY = os.environ["TRANSCRIPTFETCH_KEY"]
res = httpx.post(
"https://transcriptfetch.com/api/v1/transcripts/video",
headers={"Authorization": f"Bearer {KEY}"},
json={"video": "dQw4w9WgXcQ"}, # bare ID or any YouTube URL
timeout=60,
)
res.raise_for_status()
data = res.json()["data"]
print(data["title"])
print(data["text"]) # full transcript as plain text
for seg in data["segments"][:3]: # timestamped segments
print(f'{seg["start"]:.1f}s {seg["text"]}')
Every response shares one envelope - { ok, request_id, data, usage } - so the title, plain text, and timestamped segments come back together, and you can see credits spent in usage.
Fetch many videos in one call
The batch endpoint takes up to 50 IDs and returns one result per video, including a per-video outcome so a single missing transcript doesn't fail the whole job:
res = httpx.post(
"https://transcriptfetch.com/api/v1/transcripts/batch",
headers={"Authorization": f"Bearer {KEY}"},
json={"videoIds": ["dQw4w9WgXcQ", "9bZkp7q19f0"]},
timeout=120,
)
for item in res.json()["data"]["results"]:
if item["outcome"] == "ok":
print(item["video_id"], len(item["text"]), "chars")
else:
print(item["video_id"], "->", item["outcome"]) # e.g. no_transcript
Whole channels, playlists, and search
You don't need the IDs up front. List a channel (or playlist, or keyword search), then feed the IDs straight into a batch call:
def post(path, body):
return httpx.post(
f"https://transcriptfetch.com/api/v1/transcripts/{path}",
headers={"Authorization": f"Bearer {KEY}"},
json=body,
timeout=180,
).json()["data"]
# 1) list the channel's videos (use next_cursor to page past 50)
listing = post("channel", {"channel": "@lexfridman", "limit": 50})
ids = [v["videoId"] for v in listing["videos"]]
# 2) fetch all of them in one batch
batch = post("batch", {"videoIds": ids[:50]})
print(f'{len(batch["results"])} transcripts fetched')
Swap "channel" for "playlist" ({"playlist": "PL…"}) or "search" ({"query": "how transformers work"}) - same shape, same flow.
Which should you use?
youtube-transcript-api | Managed API | |
|---|---|---|
| Cost | Free | Credit-based (free tier to start) |
| Runs on servers/CI | Blocked without your own proxies | Yes - proxies handled |
| Video metadata | No | Title, channel, duration |
| Channel / playlist / search | No | Yes |
| Batch fetching | Build it yourself | One call, up to 50 |
| Maintenance | You own breakage + retries | Handled |
Use the free library for prototypes, notebooks, and small personal scripts. Reach for a managed API when transcripts feed something that has to keep working - a production app, a scheduled job, or a RAG/summarization pipeline pulling from whole channels.
If you want to try the managed path, TranscriptFetch has a free tier and one-credit-per-fetch pricing (failed and caption-less videos are free), plus an MCP server if you'd rather pull transcripts straight into Claude or Cursor. You can also test any single video, no code required, with the free YouTube transcript generator.
