youtube-transcript-api review
youtube-transcript-api is a synchronous Python client for retrieving caption tracks exposed by YouTube's web client. It takes a video ID, selects preferred manual or generated languages, can request available YouTube translations, and returns timed snippet objects. Built-in formatters emit text, JSON, CSV, SRT, or WebVTT without a browser or Google API key. Version 1.2.4 fixes a Webshare proxy username bug that could append the `-rotate` suffix twice. The library depends on an undocumented YouTube interface, so its Python API can stay unchanged while upstream access breaks.
youtube-transcript-api 1.2.4 installed in 0.4 seconds, used 5 MB, imported in 0.53 seconds, and had no audit findings on our box. It is a practical best-effort caption client; do not promise availability without handling blocked egress, upstream changes, and videos with no usable track.
We installed it
| Install | ✓ · 0.4s | 7 packages on disk · 5 MB |
| Import | ✓ | import youtube_transcript_api in 0.53s · pure Python · py.typed · requires Python >=3.8,<3.15 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does youtube-transcript-api install cleanly?
Yes. In a fresh container with an empty cache, pip install youtube-transcript-api finished in 0.4s, leaving 7 packages and 5 MB on disk. pip-audit reported no known vulnerabilities.
What does youtube-transcript-api need to run?
Python >=3.8,<3.15, and nothing compiled: it is pure Python. In our run import youtube_transcript_api succeeded in 0.53s, and the package ships py.typed for type checkers.
youtube-transcript-api or yt-dlp: which should you use?
yt-dlp: Use it when the job also needs subtitle files, media formats, playlists, metadata, or audio extraction. youtube-transcript-api 1.2.4 installed in 0.4 seconds, used 5 MB, imported in 0.53 seconds, and had no audit findings on our box.
When should you not use youtube-transcript-api?
A production contract requires a supported upstream API. The README says this client uses undocumented YouTube behavior that can change without notice.
Use it if
- A Python job needs best-effort captions and timings from ordinary public YouTube videos at modest volume.
- Language preference, manual-versus-generated selection, or YouTube-provided translation is part of the workflow.
- Downstream code benefits from list-like snippet objects or ready-made subtitle and text formats.
- The service can classify missing tracks separately from blocking and has an egress or proxy plan for deployment.
- A production contract requires a supported upstream API. The README says this client uses undocumented YouTube behavior that can change without notice.
- You deploy on common cloud-provider IP ranges without alternate egress. The project warns that AWS, Google Cloud, Azure, and similar addresses are often blocked.
- Age-restricted or signed-in captions are required. Cookie authentication is documented as unavailable after YouTube changes broke it.
- Videos may have no caption track and you expect speech recognition. This package fetches existing YouTube captions and does not transcribe audio.
- You need an async client or one shared session across concurrent workers. Calls use `requests.Session`; isolate clients per thread and put concurrency outside the package.
Setup reality
Our fresh Python 3.12 Bookworm install of youtube-transcript-api 1.2.4 completed in 0.4 seconds. It left 7 packages using 5 MB, and import youtube_transcript_api worked in 0.53 seconds. We measured 2 direct dependencies, pure-Python packaging with py.typed, an MIT license, and 0 known vulnerabilities from pip-audit. The supported Python range is 3.8 through versions below 3.15.
Basic fetching needs neither Google credentials nor a headless browser. Pass the video ID rather than the full watch URL, and pass preferred languages as a list even when it contains one code. The client calls an undocumented YouTube web interface. Consent behavior, playability checks, caption URLs, PO-token rules, and blocking can change outside the package's release schedule, so exception monitoring is part of setup.
Cloud egress is the common deployment failure. The README says known provider addresses are widely blocked and recommends rotating residential proxies as the most reliable workaround. Its Webshare adapter needs a username and password; generic HTTP, HTTPS, or SOCKS proxy URLs may also contain credentials. Keep those values out of source and logs. Version 1.2.4 specifically prevents a Webshare username ending in -rotate from receiving that suffix twice. A proxy can still be blocked.
Requests are synchronous and each client owns or receives a requests.Session, which caches cookies across its calls. Create separate clients for worker threads instead of sharing mutable session state. Handle missing transcripts, disabled captions, unavailable videos, and IP blocking as different outcomes; retrying cannot create a track that the video lacks. Snippet duration is on-screen caption time rather than exact speech timing, snippets may overlap, and generated or automatically translated text needs review before publication.
Patterns
Fetch the preferred English track fetch-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 a video ID, not a watch URL. `fetch()` is synchronous and English is the default language preference.
Try caption languages in order prefer-transcript-languages
transcript = api.fetch(
video_id,
languages=['de', 'en'],
)
print(transcript.language_code, transcript.is_generated)The first available language wins, with manual captions preferred when both manual and generated tracks match.
Convert snippets into dictionaries convert-to-raw-data
transcript = api.fetch(video_id)
rows = transcript.to_raw_data()
process_rows(rows)Each row carries text, start, and duration. Duration records display time; it is not a precise speech segment length, and rows may overlap.
Retain supported caption tags preserve-caption-formatting
transcript = api.fetch(
video_id,
languages=['en'],
preserve_formatting=True,
)
htmlish_text = '\n'.join(item.text for item in transcript)Selected tags such as bold and italics remain. Treat the returned caption text as untrusted before rendering it as HTML.
Inspect caption-track metadata first list-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 retrieves track metadata. Call `fetch()` only on the selected track to avoid unnecessary caption requests.
Require a manually authored track choose-manual-captions
tracks = api.list(video_id)
track = tracks.find_manually_created_transcript(['fr', 'en'])
transcript = track.fetch()This raises `NoTranscriptFound` when no manual match exists. It does not silently accept an automatically generated track.
Request a YouTube caption translation translate-transcript
tracks = api.list(video_id)
source = tracks.find_transcript(['en'])
if source.is_translatable:
german = source.translate('de').fetch()The target code must appear in `translation_languages`. Output comes from YouTube's translation, so publication still needs language review.
Write the fetched timing as SRT format-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 supplied timings. It does not correct overlapping snippets, punctuation, or recognition errors.
Configure the Webshare adapter configure-rotating-proxy
import os
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'],
))The README recommends Webshare's rotating Residential plan. Version 1.2.4 fixes duplicate `-rotate` suffixes in usernames.
Route calls through generic proxy URLs configure-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 IPs can also be blocked. Redact URLs because user information may be embedded in them.
Inject a configured requests session customize-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)The client reuses this Session and its cookies. Avoid sharing the mutable session across worker threads.
Classify content and egress failures handle-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)Missing captions and removed videos are durable content states. IP blocking is an egress condition and belongs on a different retry path.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| yt-dlp | PyPI | Use it when the job also needs subtitle files, media formats, playlists, metadata, or audio extraction. |
| pytubefix | PyPI | Use it for broader Python access to streams and video metadata alongside captions. |
| google-api-python-client | PyPI | Use it for videos you control when OAuth, quotas, and the supported YouTube Data API are required. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

