Get YouTube Transcripts in Python: The Complete Guide
David Boulen · 6/26/2026 · 8 min read
Get YouTube Transcripts in Python: The Complete Guide
YouTube users upload more than 500 hours of video every minute (Global Media Insight, 2026). Buried inside all that video is text you actually want: lecture notes, product walkthroughs, interview quotes, training data for your next model. This guide shows you how to pull that text with Python, cleanly and repeatably.
You'll build a small reusable client, fetch and parse a single transcript, loop through a whole channel with backoff, persist results, and debug the errors that quietly break most scrapers. Code is copy-paste ready.
Key Takeaways
- A reusable
requestsclient plus a hosted transcript API is the most reliable way to pull transcripts at scale.- With 500+ hours uploaded per minute, your pipeline needs backoff, retries, and block handling to survive.
- Save raw JSON before cleanup so you never re-fetch and re-pay for the same video.
- Most failures fall into four buckets — blocked IPs, missing captions, bad IDs, rate limits — each with a clear fix.
Why not just use youtube-transcript-api?
The popular open-source youtube-transcript-api library has over 7,800 GitHub stars and needs no API key, which makes it the obvious first stop. It works great on your laptop. It tends to fall apart in production.
The reason is simple: YouTube fingerprints datacenter IPs and rate-limits aggressive callers. Once your script runs from a server or fires off a few hundred requests, you start seeing empty results, 429 responses, and RequestBlocked exceptions. We've watched a perfectly good script go from 100% success to near-zero overnight after a deploy to a cloud VM.
You have two honest options. Run the OSS library and bolt on residential proxies, retry logic, and request pacing yourself. Or call a hosted transcript API that handles blocks, proxies, and retries behind a single token. This guide uses the hosted approach for the network calls, but every pattern here — the client, the backoff loop, the storage — applies either way.
For a deeper look at why the library breaks, see Why youtube-transcript-api Gets Blocked (and What to Do).

Installing requests and structuring a reusable client
Start with one dependency. The requests library is the de facto standard for HTTP in Python, and you don't need anything heavier for this job.
pip install requests
Don't scatter raw requests.get() calls across your codebase. Wrap them in a small client class. This gives you one place for the base URL, auth header, and a shared session — which reuses TCP connections and speeds up bulk runs.
import requests
class TranscriptClient:
def __init__(self, api_key, base_url="https://api.getyoutubetranscriber.com"):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
})
def get_transcript(self, video_id, lang="en"):
url = f"{self.base_url}/v1/transcript"
resp = self.session.get(
url,
params={"video_id": video_id, "lang": lang},
timeout=30,
)
resp.raise_for_status()
return resp.json()
A single Bearer token authenticates every call, and a 30-second timeout stops a slow response from hanging your whole loop. Keep your key in an environment variable, never hardcoded:
import os
client = TranscriptClient(api_key=os.environ["YT_TRANSCRIBER_KEY"])
Best practice: Use one
requests.Sessionper client instance, not one per request. Connection reuse cuts latency noticeably when you're pulling hundreds of transcripts.
Fetching a single transcript and parsing JSON
A transcript request returns structured JSON: a list of segments, each with the spoken text, a start time, and a duration. That shape is far easier to work with than a raw .srt or .vtt caption file you'd otherwise have to parse by hand.
Here's a single fetch using the client from above:
data = client.get_transcript("dQw4w9WgXcQ", lang="en")
for segment in data["transcript"]:
print(f"[{segment['start']:.1f}s] {segment['text']}")
A typical JSON response looks like this:
{
"video_id": "dQw4w9WgXcQ",
"language": "en",
"transcript": [
{ "text": "Never gonna give you up", "start": 0.0, "duration": 3.2 },
{ "text": "Never gonna let you down", "start": 3.2, "duration": 3.0 }
]
}
Need the full text as one string for an LLM prompt or a summary? Join the segments:
full_text = " ".join(seg["text"] for seg in data["transcript"])
YouTube auto-captions cover 80+ languages, and good transcript APIs expose 125+ (YouTube Help, 2025). Pass the lang you need and check what the response actually returns — a video may not have your requested track. For a guided walkthrough of the JSON shape, see How to Get a YouTube Transcript as JSON in 5 Minutes.
Looping through video lists with backoff
Pulling one transcript is easy. Pulling a thousand without getting blocked or hammering the API on transient failures is where most scripts go wrong. The fix is exponential backoff: when a call fails, wait a bit, then wait longer each retry.
import time
def get_with_backoff(client, video_id, max_retries=5):
delay = 1.0
for attempt in range(max_retries):
try:
return client.get_transcript(video_id)
except requests.HTTPError as e:
status = e.response.status_code
# Don't retry client errors that won't fix themselves
if status in (400, 401, 404):
raise
# 429 / 5xx are worth retrying
time.sleep(delay)
delay *= 2
except requests.RequestException:
time.sleep(delay)
delay *= 2
raise RuntimeError(f"Failed after {max_retries} retries: {video_id}")
Now loop over a list of IDs and keep going even when one video has no captions:
video_ids = ["dQw4w9WgXcQ", "9bZkp7q19f0", "kJQP7kiw5Fk"]
results = {}
for vid in video_ids:
try:
results[vid] = get_with_backoff(client, vid)
print(f"ok: {vid}")
except Exception as err:
print(f"skip {vid}: {err}")
time.sleep(0.5) # be a polite caller
Gotcha: Don't retry
400,401, or404. A bad video ID or a missing caption track will never succeed, so retrying just wastes time and credits. Only back off on429and5xx.
This polite pacing matters at YouTube's scale. With users watching 1 billion hours of video daily (Global Media Insight, 2026), the platform watches request patterns closely — bursty, mechanical traffic is exactly what gets flagged.

Saving transcripts to disk or a database
Fetch once, store immediately. Every transcript call costs time and, on a paid API, money — so persist the raw JSON before you do any cleanup. If your parsing logic changes later, you re-run it locally instead of re-fetching every video.
The simplest durable option is one JSON file per video:
import json
from pathlib import Path
OUT = Path("transcripts")
OUT.mkdir(exist_ok=True)
def save_json(video_id, data):
(OUT / f"{video_id}.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
The ensure_ascii=False flag keeps non-English characters readable instead of escaping them — important when you're pulling transcripts across those 125+ languages.
For anything you'll query later, use SQLite. It ships with Python, needs no server, and handles thousands of rows comfortably:
import sqlite3
conn = sqlite3.connect("transcripts.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS transcripts (
video_id TEXT PRIMARY KEY,
language TEXT,
full_text TEXT,
raw_json TEXT
)
""")
def save_db(video_id, data):
full_text = " ".join(s["text"] for s in data["transcript"])
conn.execute(
"INSERT OR REPLACE INTO transcripts VALUES (?, ?, ?, ?)",
(video_id, data.get("language"), full_text, json.dumps(data)),
)
conn.commit()
Storing both the joined full_text and the raw_json gives you the best of both worlds: fast text search now, full segment timing later. This pattern is why teams feeding RAG and LLM pipelines lean on transcripts — the retrieval-augmented generation market is projected to grow from $1.94B in 2025 to $9.86B by 2030 (MarketsandMarkets, 2025), and clean video text is increasingly part of that data.
Common errors and how to debug them
Most transcript failures fall into four buckets. Knowing which one you're hitting saves hours of guessing.
1. Blocked IP / bot challenge. Symptoms: sudden 429 floods, empty transcripts, or RequestBlocked after a deploy. Cause: YouTube flagged your datacenter IP. Fix: route through residential proxies, slow your request rate, or use a hosted API that manages proxies for you. This is the single most common production failure.
2. No captions available. Symptoms: a 404 or an empty transcript array. Cause: the video genuinely has no captions in your requested language. Fix: fall back to a different lang, or skip and log the ID.
3. Bad video ID. Symptoms: consistent 400 or 404. Cause: you passed a full URL instead of the 11-character ID, or the video is private/deleted. Fix: extract just the ID before calling.
import re
def extract_id(url_or_id):
m = re.search(r"(?:v=|youtu\.be/|/shorts/)([\w-]{11})", url_or_id)
return m.group(1) if m else url_or_id
4. Rate limit. Symptoms: intermittent 429 under heavy load. Cause: too many requests too fast. Fix: the backoff loop above, plus a small time.sleep() between calls.
When something breaks, log the status code and response body before anything else:
try:
data = client.get_transcript(vid)
except requests.HTTPError as e:
print(f"{e.response.status_code}: {e.response.text[:200]}")
That two-line habit turns most "it just doesn't work" mysteries into a five-second diagnosis. Pay only for calls that succeed and these failures stop costing you anything beyond a retry.
Wrapping up
You now have the full loop: a reusable requests client, single-transcript parsing, a backoff-protected bulk loop, durable storage, and a debugging playbook for the four failures that matter. The same patterns scale from three videos to thirty thousand.
If you'd rather not babysit proxies and retries, YouTube Transcriber handles the IP blocks, anti-bot challenges, and 125+ languages behind one Bearer token, with 100 free credits and no credit card to start. Point your new client at the Python docs and pull your first transcript in a couple of minutes.