YouTube Transcripts

How to Extract a YouTube Transcript: A Developer's Complete Guide

··7 min read

If you want to pull the text out of a YouTube video, you have two solid programmatic routes: the youtube-transcript-api Python library (fastest for individual videos and playlist loops) and yt-dlp (more resilient for bulk subtitle downloads). This guide covers both, including error handling, rate-limit strategies, and cleaning the raw output into something useful.


The Fastest Method: youtube-transcript-api

Short answer: Install youtube-transcript-api via pip, instantiate YouTubeTranscriptApi(), call .fetch(video_id), and you get a FetchedTranscript with attribute-based snippet access — no API key needed.

pip install youtube-transcript-api
from youtube_transcript_api import YouTubeTranscriptApi

video_id = "dQw4w9WgXcQ"  # the part after ?v=
ytt_api = YouTubeTranscriptApi()
fetched = ytt_api.fetch(video_id)

for snippet in fetched:
    print(f"[{snippet.start:.1f}s] {snippet.text}")

The library scrapes YouTube's internal timedtext endpoint. It works on any video that has captions enabled — auto-generated or manually uploaded. No OAuth, no quota.

Flatten to plain text

For most downstream use cases (summarization, keyword extraction, RAG chunking) you want a single string:

full_text = " ".join(snippet.text for snippet in fetched)

List available languages

If a video has multiple caption tracks, pick the one you want:

from youtube_transcript_api import YouTubeTranscriptApi

ytt_api = YouTubeTranscriptApi()
transcript_list = ytt_api.list(video_id)

for t in transcript_list:
    print(t.language, t.language_code, "auto" if t.is_generated else "manual")

# Fetch a specific language
fetched = transcript_list.find_transcript(["en"]).fetch()

Handling Videos With No Transcript

Short answer: Wrap the call in a try/except catching NoTranscriptFound and TranscriptsDisabled — then decide whether to skip, log, or fall back to audio transcription.

Not every video has captions. About 30–40% of YouTube videos have no transcript at all. You need to handle three exceptions:

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

ytt_api = YouTubeTranscriptApi()

def safe_get_transcript(video_id: str) -> str | None:
    try:
        fetched = ytt_api.fetch(video_id)
        return " ".join(snippet.text for snippet in fetched)
    except TranscriptsDisabled:
        print(f"{video_id}: captions disabled by uploader")
    except NoTranscriptFound:
        print(f"{video_id}: no transcript in requested language")
    except VideoUnavailable:
        print(f"{video_id}: video is private or deleted")
    return None

If captions are disabled and you need the text anyway, whisper (OpenAI) or faster-whisper can transcribe the audio — but that adds significant complexity (download audio with yt-dlp, run ASR, handle alignment).


Handling Rate Limits

Short answer: YouTube will start returning 429s if you hammer requests without delays. Add jitter between calls and use exponential backoff — see the pattern below and the dedicated YouTube transcript rate limit guide.

The library makes direct HTTP requests to YouTube. At scale, you will hit rate limits. The symptoms: requests.exceptions.HTTPError: 429 or responses that return empty transcripts silently.

import time
import random
from youtube_transcript_api import YouTubeTranscriptApi, IpBlocked

ytt_api = YouTubeTranscriptApi()

def get_transcript_with_backoff(video_id: str, max_retries: int = 4) -> str | None:
    for attempt in range(max_retries):
        try:
            fetched = ytt_api.fetch(video_id)
            return " ".join(snippet.text for snippet in fetched)
        except IpBlocked:
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f"Rate limited. Waiting {wait:.1f}s before retry {attempt + 1}")
            time.sleep(wait)
            continue
        except Exception as e:
            print(f"{video_id} failed: {e}")
            return None
    return None

For bulk jobs (hundreds of videos), add a baseline delay of 1–2 seconds between requests regardless:

time.sleep(random.uniform(1.0, 2.5))

Proxies and rotating IPs can help for very high volume, but for most single-developer use cases (channel archives, research pipelines) rate-limited retries are enough.


Looping Over a Channel or Playlist

Short answer: Use googleapiclient or the scrapetube library to pull video IDs from a channel or playlist, then loop your transcript extractor over the list.

The youtube-transcript-api takes individual video IDs. Getting the list of IDs for a channel or playlist requires a separate step. The cleanest no-auth option is scrapetube:

pip install scrapetube
import scrapetube
import time
import random
from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound

channel_url = "https://www.youtube.com/@mkbhd"

ytt_api = YouTubeTranscriptApi()
results = []
for video in scrapetube.get_channel(channel_url=channel_url):
    video_id = video["videoId"]
    try:
        fetched = ytt_api.fetch(video_id)
        text = " ".join(snippet.text for snippet in fetched)
        results.append({"video_id": video_id, "transcript": text})
        print(f"OK: {video_id} ({len(text)} chars)")
    except (TranscriptsDisabled, NoTranscriptFound):
        print(f"SKIP: {video_id} — no transcript")
    except Exception as e:
        print(f"ERROR: {video_id} — {e}")
    time.sleep(random.uniform(1.2, 2.8))

print(f"\nCollected {len(results)} transcripts")

For playlists, swap scrapetube.get_channel(...) for scrapetube.get_playlist(playlist_id="PLxxx").

If you need video metadata (title, publish date, view count) alongside transcripts, the YouTube Data API v3 is the right tool — but it requires an API key and has its own quota limits (10,000 units/day free).


The yt-dlp Approach: Subtitle File Download

Short answer: yt-dlp --write-auto-subs --skip-download -o "%(id)s.%(ext)s" downloads the .vtt subtitle file to disk — useful when you want offline archives or need resilience to API changes.

yt-dlp is the community-maintained successor to youtube-dl. It downloads subtitle files in .vtt or .srt format directly. No Python library required, and it tends to be more resilient to YouTube's internal changes. See the yt-dlp YouTube subtitles deep dive for full flag reference.

# Download auto-generated English subtitles only (no video)
yt-dlp \
  --write-auto-subs \
  --sub-langs en \
  --skip-download \
  --output "%(id)s.%(ext)s" \
  "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

This produces a file like dQw4w9WgXcQ.en.vtt.

Parse the VTT in Python

import re

def parse_vtt(filepath: str) -> str:
    with open(filepath, "r", encoding="utf-8") as f:
        raw = f.read()

    # Strip VTT header and timestamps
    lines = raw.split("\n")
    text_lines = []
    for line in lines:
        line = line.strip()
        if not line:
            continue
        if line.startswith("WEBVTT") or line.startswith("NOTE"):
            continue
        if re.match(r"^\d{2}:\d{2}:\d{2}", line):  # timestamp line
            continue
        if re.match(r"^\d+$", line):  # sequence number
            continue
        text_lines.append(line)

    # Deduplicate adjacent repeated lines (common in auto-captions)
    deduped = [text_lines[0]] if text_lines else []
    for line in text_lines[1:]:
        if line != deduped[-1]:
            deduped.append(line)

    return " ".join(deduped)

transcript = parse_vtt("dQw4w9WgXcQ.en.vtt")

The deduplication step matters — YouTube's auto-captions overlap lines in the VTT format, so without it you get repeated fragments throughout the text.

For playlist downloads, pass a playlist URL to yt-dlp with the same flags and it handles the loop internally.


Cleaning the Raw Output

Short answer: Strip [Music], [Applause], and similar bracketed tags; collapse whitespace; optionally remove filler words before passing to your LLM or search index.

Auto-generated transcripts come with noise:

import re

def clean_transcript(text: str) -> str:
    # Remove bracketed sound tags: [Music], [Applause], [Laughter]
    text = re.sub(r"\[.*?\]", "", text)
    # Collapse multiple spaces/newlines
    text = re.sub(r"\s+", " ", text)
    return text.strip()

For serious NLP pipelines, also consider sentence segmentation — raw transcripts have no punctuation. Libraries like punctuator2 or an LLM call can add it back, which significantly improves chunking quality for RAG.


When You Want Meaning, Not Just Raw Text

Building and maintaining a transcript pipeline is real work: rate limits, missing captions, VTT parsing edge cases, ASR fallbacks, and then still needing to do something useful with the text.

If your actual goal is to understand what a video is about — get a summary, ask a specific question, or monitor a channel for topics you care about — that's a separate problem from extraction. SkipTheWatch handles the full pipeline without any setup: paste a URL, get an AI summary and the ability to ask questions directly about the video's content. There's no browser extension to install and no code to maintain.

It's not a replacement for programmatic access — if you're building your own pipeline, the methods above are what you need. But if you're doing research, competitive monitoring, or content triage and you just want the meaning without the infrastructure, it's worth knowing the option exists. $9/month after a 7-day free trial. Cancel anytime in the trial for $0. A card is required at sign-up.

For deeper background on the library internals, see the youtube-transcript-api guide and the Python-specific walkthrough.


Frequently Asked Questions

Does youtube-transcript-api require an API key? No. It scrapes YouTube's internal timedtext endpoint directly. No Google Cloud project, no OAuth, no quota tracking required. The tradeoff is that it can break if YouTube changes its internal API — the library is community-maintained and typically patches within days of a breaking change.

What happens if a video has no captions at all? You'll get a NoTranscriptFound or TranscriptsDisabled exception. Your options are: skip the video, log it for manual review, or fall back to audio-based transcription using a tool like faster-whisper (which requires downloading the audio first with yt-dlp).

How do I get transcripts for an entire YouTube channel? Use scrapetube to iterate over a channel's video IDs, then call ytt_api.fetch() on each one with a sleep/backoff between calls. See the loop example above. Expect 30–40% of videos to have no transcript depending on the channel.

Why does my VTT file have duplicate lines? YouTube's auto-caption VTT format overlaps caption cues for display smoothness. Each logical phrase appears in two consecutive cue blocks. The deduplication step in the parser above removes these — compare adjacent lines and drop repeats before joining.

What's the difference between auto-generated and manual transcripts? Manual transcripts are uploaded by the video creator and tend to be more accurate. Auto-generated ones use YouTube's ASR and often miss proper nouns, technical terms, and speaker changes. For most extraction use cases the auto-generated version is sufficient — but for high-accuracy pipelines (legal, medical, academic) plan on a post-processing correction step.

Skip the watch. Get the insight.

AI summaries of any YouTube video in seconds — key insights, timestamps, structured breakdowns. Start free — no credit card. Pro is $9/month for 500 summaries with a 7-day free trial, cancel anytime.

Get my free summaries

Free to start · No credit card · Upgrade anytime

Related Articles