The short answer: there are three working ways to get YouTube transcripts in Python in 2026. The open-source youtube-transcript-apilibrary is free and excellent for prototypes and low volume, but YouTube blocks requests from datacenter IP ranges (the library's own docs describe the IP bans), so it degrades once you deploy. The official YouTube Data API only lets you download captions for videos your own channel uploaded. For unattended pipelines at volume, a managed pay-per-result actor is the option that doesn't hand you a proxy-management side quest. This article walks through all three with real error handling, and says plainly when the free option is the right one.
First, know what a "transcript" actually is
YouTube transcripts are caption tracks: either uploaded by the creator (manual) or generated by YouTube's speech recognition (auto-generated / ASR). Every method below reads those existing tracks; none of them transcribes audio. A video with no caption track has nothing to fetch, and your code needs to expect that case: some of any random video list will have no fetchable transcript.
Option 1: youtube-transcript-api (free, best for prototypes)
The youtube-transcript-api package is the standard open-source choice and deserves its popularity:
pip install youtube-transcript-apifrom youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api._errors import (
TranscriptsDisabled,
NoTranscriptFound,
VideoUnavailable,
)
ytt = YouTubeTranscriptApi()
def fetch_transcript(video_id: str):
try:
fetched = ytt.fetch(video_id, languages=["en"])
return [
{"text": s.text, "start": s.start, "duration": s.duration}
for s in fetched
]
except TranscriptsDisabled:
return None # creator turned captions off
except NoTranscriptFound:
return None # no track in the languages you asked for
except VideoUnavailable:
return None # deleted, private, or region-locked
segments = fetch_transcript("jNQXAC9IVRw")
print(segments[0])
# {'text': 'All right, so here we are, in front of the elephants',
# 'start': 1.2, 'duration': 2.16}This is genuinely good software, and if you are pulling a few hundred transcripts from your laptop for a research notebook, use it and stop reading.
Where it starts to hurt:
- Cloud IPs get blocked.YouTube aggressively blocks requests from datacenter IP ranges (AWS, GCP, Azure, Hetzner…). The library's own documentation recommends rotating residential proxies for anything deployed. The code that worked on your laptop starts returning IP-block errors in production.
- You own the retries, proxies, and rate limits. Once you add a proxy provider, backoff logic, and monitoring, "free" has a real engineering cost.
- No SLA and no batching. One video per call, and when YouTube changes an internal response format, you wait for a patch release.
Option 2: the official YouTube Data API (only for your own videos)
The intuitive move, "just use the official API", dead-ends quickly: per Google's documentation, the Data API's captions.download endpoint only serves requests authorized by the owner of (or a party with edit permission on) the video. An API key is not enough, and third-party caption download for arbitrary public videos is restricted. It works if you are exporting captions from your own uploads, and not otherwise. The full explanation, including the quota math, is in Why the YouTube Data API won't give you transcripts.
Option 3: a managed pay-per-result actor (for unattended pipelines)
The third option outsources the proxy war: a managed extractor, running on infrastructure whose job is to keep working, priced per delivered transcript. The example below uses the YouTube Transcript Scraper on Apify via the official apify-client SDK ($3.00 per 1,000 transcripts; a video with no fetchable captions returns success: false and costs $0):
pip install apify-clientimport os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("apihq/youtube-transcript-scraper").call(
run_input={
"videoIds": ["jNQXAC9IVRw", "dQw4w9WgXcQ", "00000000000"],
"language": "en",
"metadata": True,
}
)
transcripts, misses = [], []
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["success"]:
transcripts.append(item) # billed: $0.003
else:
misses.append(item) # free, machine-readable
print(item["video_id"], item["code"])
# e.g. "00000000000 PLAYABILITY"
print(f"{len(transcripts)} transcripts, {len(misses)} misses")The part that matters for a pipeline is the failure contract, not the happy path:
- A bad video does not abort the run. A captionless or unplayable video becomes a
success: falserow in the same dataset instead of an exception at 3 a.m. - Failures carry a stable machine-readable code (
NO_CAPTIONS,PLAYABILITY,DEADLINE_EXCEEDED), so you branch in code, not by parsing error strings. - Failures are never billed. You pay per delivered transcript, so a batch of dead IDs costs $0.
- A hard 30-second deadline per video means a stalled upstream produces a clean coded failure, not a hung worker.
Scaling up: transcripts for a whole channel
The transcript actor takes video IDs (up to 50 per run), so for a whole channel you chain two steps: list the channel's videos first with the YouTube Channel Scraper ($0.50 per 1,000 videos), then feed the IDs through in batches:
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
# 1. Channel -> video IDs
listing = client.actor("apihq/youtube-channel-scraper").call(
run_input={"channelUrl": "@mkbhd", "maxResults": 200}
)
video_ids = [
item["video_id"]
for item in client.dataset(listing["defaultDatasetId"]).iterate_items()
if item["success"]
]
# 2. Video IDs -> transcripts, 50 per run
def chunks(xs, n=50):
for i in range(0, len(xs), n):
yield xs[i : i + n]
corpus = []
for batch in chunks(video_ids):
run = client.actor("apihq/youtube-transcript-scraper").call(
run_input={"videoIds": batch, "language": "en"}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["success"]:
text = " ".join(s["text"] for s in item["transcript"])
corpus.append({"video_id": item["video_id"], "text": text})
print(f"Corpus: {len(corpus)} documents")Cost check for a 200-video channel where, say, 180 videos have captions: 200 listed videos = $0.10, plus 180 delivered transcripts = $0.54. The 20 captionless videos cost nothing. Total: about $0.64 for a channel-sized RAG corpus.
Which one should you use?
| Situation | Use | Why |
|---|---|---|
| Notebook, research, a few hundred videos from your laptop | youtube-transcript-api | Free and simple when run from a residential IP |
| Captions for videos your own channel uploaded | YouTube Data API | Official, supported, and you can authorize as the owner |
| Unattended pipeline, cloud deployment, or real volume | Managed actor | No proxy management, per-result billing, coded failures that never kill a batch |
The free library and the paid actor are not competitors. They do the same job at different stages of maturity. Prototype with the library. When the pipeline needs to run without you watching it, move the fetch behind a contract that fails safely and bills only on delivery. We ran both tools on the same 11 videos and published the row-by-row results in a first-hand comparison, including the cases where the library is the better choice.