Skip to content

How to Use youtube-transcript-api in Python (With Examples)

Install, fetch, filter by language, batch-process channels, and handle every common error with the youtube-transcript-api Python library. Copy-paste examples.

TL;DR: pip install youtube-transcript-api, call YouTubeTranscriptApi.get_transcript(video_id), get back a list of {text, start, duration} dicts. The rest of this post covers language selection, batch processing, proxy setup, error handling, and the patterns that survive past a hundred videos.

This is the Python library at github.com/jdepoix/youtube-transcript-api. It’s the most common way to pull YouTube captions in code, and it works for most prototypes in under five minutes.

If you’re trying to decide between this library and a hosted API, read the complete guide first. This post assumes you’ve decided to use the library and just want working code.

Install

pip install youtube-transcript-api

Latest version supports Python 3.8+. There are no system dependencies.

Pull a Transcript

The simplest call:

from youtube_transcript_api import YouTubeTranscriptApi

video_id = "dQw4w9WgXcQ"
transcript = YouTubeTranscriptApi.get_transcript(video_id)

for segment in transcript[:3]:
    print(segment)

Output:

{"text": "We're no strangers to love", "start": 18.8, "duration": 4.7}
{"text": "You know the rules and so do I", "start": 23.5, "duration": 3.2}
{"text": "A full commitment's what I'm thinking of", "start": 26.7, "duration": 4.1}

The video_id is the 11-character string after v= in a YouTube URL, or the path segment in a youtu.be/ short link. If you have a full URL, parse it with urllib:

from urllib.parse import urlparse, parse_qs

def extract_video_id(url):
    parsed = urlparse(url)
    if parsed.hostname in ("youtu.be",):
        return parsed.path.lstrip("/")
    return parse_qs(parsed.query).get("v", [None])[0]

Get Flat Text Instead of Segments

The library returns segments. To collapse them into a single string for an LLM prompt or full-text search:

transcript = YouTubeTranscriptApi.get_transcript(video_id)
full_text = " ".join(seg["text"] for seg in transcript)

For timestamped output (useful for video summaries with citations):

from datetime import timedelta

def format_with_timestamps(transcript):
    lines = []
    for seg in transcript:
        ts = str(timedelta(seconds=int(seg["start"])))
        lines.append(f"[{ts}] {seg['text']}")
    return "\n".join(lines)

Select a Language

Pass an ordered list of language codes. The library returns the first one that exists.

transcript = YouTubeTranscriptApi.get_transcript(
    video_id,
    languages=["es", "pt", "en"]
)

If none of the requested languages exist, the library raises NoTranscriptFound. Always wrap in a try/except:

from youtube_transcript_api import NoTranscriptFound

try:
    transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=["es"])
except NoTranscriptFound:
    transcript = YouTubeTranscriptApi.get_transcript(video_id)  # fall back to default

Discover Available Tracks Before Fetching

If you don’t know what languages a video has, list first:

transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)

for t in transcript_list:
    print(f"{t.language} ({t.language_code}) — generated={t.is_generated}")

The is_generated flag separates auto-captions from manually uploaded ones. This matters for quality.

# Prefer manual captions, fall back to auto
for t in transcript_list:
    if not t.is_generated:
        transcript = t.fetch()
        break
else:
    transcript = next(iter(transcript_list)).fetch()

Translate a Caption Track

YouTube can translate caption tracks server-side. The library exposes this:

transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
english = transcript_list.find_transcript(["en"])

# Translate to French
french = english.translate("fr").fetch()

Only tracks where is_translatable is True support this. Translation quality is YouTube’s, not a dedicated translation API. Fine for rough understanding, not for publishing.

Batch Process a List of Video IDs

The library doesn’t have a batch endpoint. You loop with backoff.

import time
from youtube_transcript_api import (
    YouTubeTranscriptApi,
    TranscriptsDisabled,
    NoTranscriptFound,
)

def batch_fetch(video_ids, delay=1.0):
    results = {}
    for vid in video_ids:
        try:
            results[vid] = YouTubeTranscriptApi.get_transcript(vid)
        except TranscriptsDisabled:
            results[vid] = None
        except NoTranscriptFound:
            results[vid] = None
        except Exception as e:
            results[vid] = {"error": str(e)}
        time.sleep(delay)
    return results

For more than a couple thousand videos, this approach hits IP blocks. Move to a hosted transcript API at that point. The migration is straightforward and the hosted version handles the IP rotation.

Use Proxies to Reduce Blocks

If you need to push the library further, route through a proxy:

from youtube_transcript_api import YouTubeTranscriptApi

proxies = {
    "http": "http://user:pass@proxy.example.com:8080",
    "https": "http://user:pass@proxy.example.com:8080",
}

transcript = YouTubeTranscriptApi.get_transcript(
    video_id,
    proxies=proxies
)

Use residential proxies, not datacenter ones. Datacenter ranges are pre-blocked by YouTube. Bright Data, Smartproxy, and Oxylabs all work but they cost more than a managed API at most volumes.

Pass Cookies for Age-Restricted Videos

Some age-restricted videos return transcripts if you pass an authenticated YouTube session cookie:

transcript = YouTubeTranscriptApi.get_transcript(
    video_id,
    cookies="/path/to/cookies.txt"
)

Export cookies.txt from a browser extension while logged into YouTube. The file format must be Netscape-compatible. Cookies eventually expire and you re-export.

This works inconsistently. For production, don’t depend on it.

Handle Every Common Error

The library raises specific exceptions. Catch them individually:

from youtube_transcript_api import (
    YouTubeTranscriptApi,
    TranscriptsDisabled,
    NoTranscriptFound,
    VideoUnavailable,
    TooManyRequests,
)

def safe_fetch(video_id):
    try:
        return YouTubeTranscriptApi.get_transcript(video_id)
    except TranscriptsDisabled:
        return {"status": "no_captions"}
    except NoTranscriptFound:
        return {"status": "language_unavailable"}
    except VideoUnavailable:
        return {"status": "private_or_removed"}
    except TooManyRequests:
        return {"status": "rate_limited"}
    except Exception as e:
        return {"status": "unknown_error", "detail": str(e)}

Quick map of what each means:

ExceptionWhat it meansRecoverable?
TranscriptsDisabledCreator turned captions offNo, skip
NoTranscriptFoundLanguage you asked for doesn’t existYes, fall back
VideoUnavailablePrivate, removed, or region-blockedNo, skip
TooManyRequestsYouTube rate-limited your IPYes, back off

Save Transcripts to Disk

For local pipelines, dump to JSON:

import json
from pathlib import Path

def save_transcript(video_id, transcript, out_dir="transcripts"):
    Path(out_dir).mkdir(exist_ok=True)
    path = Path(out_dir) / f"{video_id}.json"
    with open(path, "w") as f:
        json.dump(transcript, f, indent=2)

For SRT subtitle format (useful for video tooling):

def to_srt(transcript):
    lines = []
    for i, seg in enumerate(transcript, start=1):
        start = format_srt_time(seg["start"])
        end = format_srt_time(seg["start"] + seg["duration"])
        lines.append(f"{i}\n{start} --> {end}\n{seg['text']}\n")
    return "\n".join(lines)

def format_srt_time(seconds):
    h = int(seconds // 3600)
    m = int((seconds % 3600) // 60)
    s = int(seconds % 60)
    ms = int((seconds - int(seconds)) * 1000)
    return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"

What This Library Won’t Do

Honest list of limitations to know up front.

  • No batch endpoint. One video per request, always.
  • No rate limit headers. You learn the limit by getting blocked.
  • No retry on transient failures. Wrap calls yourself.
  • No SLA. Open-source, maintained by one person.
  • IP blocks scale linearly with usage. Around the low thousands of requests from a single IP, expect blocks.

If any of these are deal-breakers for your use case, the TubeAlfred Transcript API handles all five. Same response shape after a small adapter (covered in the pillar guide).

Frequently Asked Questions

Is youtube-transcript-api the same as youtube_transcript_api?

Yes. The pip package is youtube-transcript-api (with hyphens). The Python import is youtube_transcript_api (with underscores, per PEP 8). People search for both spellings.

Why does it sometimes return an empty list?

The video has a caption track but it’s empty (rare), or YouTube’s response format changed and the library hasn’t been updated yet. Check the GitHub issues and upgrade.

Can I use this without Python?

The library is Python-only. For other languages, either call YouTube’s caption endpoints directly (brittle), use a hosted API over HTTP from any language, or run the Python library behind a small wrapper service.

How do I avoid getting IP-blocked?

You don’t, at scale. You can delay things with proxies, cookies, and rate limiting, but the only real solution past a few thousand requests is a hosted API that handles IP rotation upstream.

Does it work with YouTube Premium content?

Public videos behind Premium paywalls (rare) won’t return transcripts to unauthenticated clients. Standard public videos work fine.

Next Steps

If this library covers your needs, you’re done. Bookmark the error handling patterns and ship.

If you’re already planning to outgrow it, read the pillar guide on YouTube transcript APIs for the migration path and side-by-side comparison with the hosted alternative.