Guides · YouTube Transcript Scraper · Published July 4, 2026

How to get YouTube video transcripts in Python: three ways, with real error handling

Three working ways to pull YouTube transcripts in Python in 2026: the open-source library, the official Data API, and a managed pay-per-result actor. Full code, real error handling, and when to use each.

By slvDev · Updated and technically verified July 11, 2026


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-api
from 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-client
import 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: false row 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?

SituationUseWhy
Notebook, research, a few hundred videos from your laptopyoutube-transcript-apiFree and simple when run from a residential IP
Captions for videos your own channel uploadedYouTube Data APIOfficial, supported, and you can authorize as the owner
Unattended pipeline, cloud deployment, or real volumeManaged actorNo 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.


Deploying this?

Run one input on the YouTube Transcript Scraper. It comes back as either a billed transcript row or an unbilled, typed failure row — the contract this guide builds on.

A one-item first run costs under a cent, on your existing Apify account.