youtube-transcript-api
youtube-transcript-api is a synchronous Python client that extracts caption tracks from YouTube's web client without an API key or headless browser. Given a video ID, it can choose preferred languages, distinguish manual from generated captions, request YouTube translations, preserve limited formatting, and emit raw dictionaries, text, JSON, SRT, or WebVTT. It also includes a CLI and proxy configuration. The convenience rests on an undocumented YouTube endpoint, not a supported public API, so availability is inherently less predictable than the simple call suggests.
Excellent for small best-effort caption jobs and scripts, with a much nicer API than scraping the page yourself. Do not build a hard availability promise on it unless you are prepared for proxy costs, upstream breakage, and videos that simply have no usable track.
Use it if
- A Python script needs public caption text and timestamps for a modest set of ordinary YouTube videos
- You need to prefer manual captions, fall back across languages, or use YouTube's available caption translations
- A data pipeline wants list-like transcript objects plus ready-made SRT, WebVTT, JSON, or plain-text formatting
- You accept a best-effort scraper and can monitor failures, throttle requests, and change network egress when YouTube blocks it
- This is a production dependency that must have a supported upstream contract: the README explicitly warns that it calls an undocumented YouTube web-client API that may stop working without notice
- You deploy from AWS, Google Cloud, Azure, or another datacenter range without a proxy plan: the README says most known cloud-provider IPs are blocked and recommends rotating residential proxies
- You need age-restricted or account-only captions: cookie authentication is explicitly disabled because recent YouTube changes broke it
- You need audio transcription when captions are absent or disabled: the package only retrieves YouTube-provided tracks and raises TranscriptsDisabled or NoTranscriptFound rather than running speech recognition
- You need async or shared multithreaded access: it uses requests, and the current constructor documentation says each instance owns a non-thread-safe Session and should be created per thread
Setup reality
pip install youtube-transcript-api requires Python 3.8 or newer and installs requests plus defusedxml. There is no Google API key, OAuth client, or browser binary. Pass a video ID, not a full watch URL, and pass language preferences as a list even when there is only one. The difficult part is network reliability. The project uses an undocumented YouTube web-client endpoint, so markup, Innertube behavior, consent flows, PO-token requirements, and caption URLs can change independently of releases. Public cloud egress is frequently blocked; the README says rotating residential proxies are the most reliable workaround and includes an affiliate-backed Webshare adapter. That adds cost, credentials, geography choices, latency, and another failure domain, and even a proxy can be blocked. Generic HTTP or HTTPS proxy URLs may include credentials and must be kept out of logs. Requests are synchronous. The API creates a requests.Session, caches cookies, and is not thread-safe, so create one client per worker thread rather than sharing a global client. You can inject a Session to set CA verification or headers, but cookie authentication for restricted videos is currently commented out and documented as unavailable. Captions may be disabled, missing in preferred languages, automatically generated, untranslatable, age restricted, blocked, or require a PO token. Handle the library's specific exceptions and keep a queue or retry policy outside it; retries cannot manufacture a transcript that does not exist. Snippet duration is how long text stays onscreen, not guaranteed speech duration, and snippets can overlap. Generated captions and automatic translations need quality review before publication or search indexing.
Patterns
Fetch a video's English transcriptfetch-default-transcript
from youtube_transcript_api import YouTubeTranscriptApi
api = YouTubeTranscriptApi()
transcript = api.fetch('dQw4w9WgXcQ')
for snippet in transcript:
print(snippet.start, snippet.duration, snippet.text)Pass the video ID, not the full URL. The default language preference is English, and the call is synchronous.
Try languages in priority orderprefer-transcript-languages
transcript = api.fetch(
video_id,
languages=['de', 'en'],
)
print(transcript.language_code, transcript.is_generated)languages is always an iterable of codes, even for one language. The first available match wins, with manual captions preferred over generated ones.
Convert result objects to dictionariesconvert-to-raw-data
transcript = api.fetch(video_id)
rows = transcript.to_raw_data()
# [{'text': '...', 'start': 0.0, 'duration': 1.54}, ...]
process_rows(rows)duration describes onscreen caption duration, not exact spoken-word duration. Adjacent snippets can overlap.
Keep supported caption markuppreserve-caption-formatting
transcript = api.fetch(
video_id,
languages=['en'],
preserve_formatting=True,
)
htmlish_text = '\n'.join(item.text for item in transcript)This preserves selected tags such as italics and bold. Treat the result as untrusted external content before rendering it as HTML.
Inspect every available caption tracklist-available-transcripts
tracks = api.list(video_id)
for track in tracks:
print({
'language': track.language,
'code': track.language_code,
'generated': track.is_generated,
'translatable': track.is_translatable,
})Listing fetches metadata, not all caption text. Call track.fetch() only for tracks the application actually needs.
Require manually created captionschoose-manual-captions
tracks = api.list(video_id)
track = tracks.find_manually_created_transcript(['fr', 'en'])
transcript = track.fetch()This raises NoTranscriptFound rather than falling back to generated captions. Use find_generated_transcript when generated text is explicitly acceptable.
Request a YouTube caption translationtranslate-transcript
tracks = api.list(video_id)
source = tracks.find_transcript(['en'])
if source.is_translatable:
german = source.translate('de').fetch()translate uses YouTube's caption translation and returns a new track descriptor. The requested language must appear in translation_languages.
Render a transcript as SRTformat-as-srt
from youtube_transcript_api.formatters import SRTFormatter
transcript = api.fetch(video_id, languages=['en'])
srt = SRTFormatter().format_transcript(transcript)
with open('captions.srt', 'w', encoding='utf-8') as output:
output.write(srt)The formatter converts existing caption timing; it does not correct overlaps, transcription errors, or machine-generated punctuation.
Use a Webshare residential proxy poolconfigure-rotating-proxy
import os
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api.proxies import WebshareProxyConfig
api = YouTubeTranscriptApi(proxy_config=WebshareProxyConfig(
proxy_username=os.environ['WEBSHARE_PROXY_USERNAME'],
proxy_password=os.environ['WEBSHARE_PROXY_PASSWORD'],
filter_ip_locations=['de', 'us'],
))
transcript = api.fetch(video_id)The README says to use the rotating Residential product, not static or ordinary proxy-server plans. Proxy use costs money and still does not guarantee access.
Route requests through a generic proxyconfigure-generic-proxy
from youtube_transcript_api.proxies import GenericProxyConfig
api = YouTubeTranscriptApi(proxy_config=GenericProxyConfig(
http_url=os.environ['HTTP_PROXY_URL'],
https_url=os.environ['HTTPS_PROXY_URL'],
))Static proxy addresses are often blocked after repeated use. URLs may contain credentials, so redact them from exception and configuration logs.
Provide a configured requests sessioncustomize-http-session
from requests import Session
session = Session()
session.headers.update({'Accept-Encoding': 'gzip, deflate'})
session.verify = '/etc/ssl/certs/private-ca.pem'
api = YouTubeTranscriptApi(http_client=session)YouTubeTranscriptApi updates the Session's Accept-Language header. A client instance and its Session are not thread-safe; create one per thread.
Separate unavailable captions from network blockinghandle-retrieval-failures
from youtube_transcript_api import (
NoTranscriptFound, TranscriptsDisabled,
RequestBlocked, IpBlocked, VideoUnavailable,
)
try:
transcript = api.fetch(video_id, languages=['en'])
except (NoTranscriptFound, TranscriptsDisabled):
transcript = None
except (RequestBlocked, IpBlocked) as error:
retry_on_other_egress(video_id, error)
except VideoUnavailable:
mark_video_removed(video_id)Do not blindly retry every exception. Missing tracks and unavailable videos are content states; IP blocking is an egress problem.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| yt-dlp | PyPI | A broader media workflow also needs subtitle files, metadata, formats, playlists, or audio extraction |
| pytubefix | PyPI | Python code needs broader video and stream access along with caption-track handling |
| google-api-python-client | PyPI | You control the videos and need the supported YouTube Data API with OAuth, quotas, and official caption resources |