Skip to main content

ytscrape — Free YouTube Scraper for Python

Scrape YouTube search results, video metadata and comments — no API key, no quota, no browser.

PyPI version Python versions Downloads License: MIT Typed API key

CI Release Ruff Last commit Stars

ytscrape is a free YouTube scraper for Python built on top of the internal YouTube InnerTube API — the same endpoints the YouTube web app uses. Use it to search YouTube, extract video, channel and playlist data, fetch video metadata and collect every comment (and reply) of a video — all with transparent pagination, typed models and no API key.

from ytscrape import YouTube

with YouTube() as yt:
    for video in yt.search("python", max_results=5):
        print(video.title, video.url)

⚠️ This library talks to YouTube's private endpoints. Use it responsibly and at your own risk; the endpoints and params values may change over time.

Table of Contents

Why ytscrape?

  • 🔑 No YouTube Data API key, no quota, no sign-up, no billing project.
  • 🧊 No browser — no Selenium, no Playwright, no headless Chrome. Pure HTTP.
  • 🧩 Typed models (Video, Channel, Playlist, VideoDetails, ChannelDetails, Comment) instead of raw, deeply-nested JSON.
  • 📄 Transparent pagination — just iterate; continuation tokens are handled for you.
  • 💬 Full comment scraping, including replies and the sort order that actually returns every comment.
  • 🌍 Localisation — interface language (hl) and content region (gl), validated with pycountry.
  • 🪶 Tiny install — only requests and pycountry.
  • 🖥️ CLI included: ytscrape search "python" --max 10.
  • 🧱 Clean, extensible OOP design (facade + factory methods + strategy) — easy to build on or to mock in tests.

Installation

pip install ytscrape

or with uv:

uv add ytscrape

From source:

pip install .

Requires Python 3.10+. Runtime dependencies: requests, pycountry.

Quick start

Everything you usually need, in one snippet:

from ytscrape import YouTube, SearchFilter, CommentSort

with YouTube(language="en", region="US") as yt:
    # 1. Search videos and iterate over as many pages as needed.
    for video in yt.search("python", filter=SearchFilter.VIDEOS, max_results=20):
        print(video.title, "-", video.url)

    # 2. Search channels.
    for channel in yt.search("python", filter=SearchFilter.CHANNELS, max_results=5):
        print(channel.title, channel.url)

    # 3. Fetch details for a single video (id or URL both work).
    details = yt.video("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
    print(details.title, details.channel, details.views, details.length_seconds)

    # 4. Collect comments — including replies and *every* comment.
    for comment in yt.comments(
        "https://youtu.be/dQw4w9WgXcQ",
        include_replies=True,
        sort=CommentSort.NEWEST,
        max_results=100,
    ):
        marker = "  ↳" if comment.is_reply else "-"
        print(f"{marker} {comment.author}: {comment.text}")

📂 More runnable snippets live in examples/.

Feature coverage

Area Status Notes
Search — videos SearchFilter.VIDEOS
Search — channels SearchFilter.CHANNELS
Search — playlists SearchFilter.PLAYLISTS
Search — Shorts / movies SearchFilter.SHORTS, SearchFilter.MOVIES
Video metadata YouTube.video() (id or any URL)
Channel metadata YouTube.channel() (id, @handle or URL)
Comments YouTube.comments(), full pagination
Comment replies include_replies=True
Comment sort (top / newest) sort=CommentSort.NEWEST returns every comment
Continuation / pagination Transparent for search and comments
Transcripts / subtitles YouTube.transcript() / transcripts()
Localisation (hl / gl) Validated ISO codes
Typed models + py.typed PEP 561 compliant
CLI python -m ytscrape / ytscrape
Channel videos / Shorts / live tabs 🚧 Planned — see Roadmap
Playlist items 🚧 Planned
Related videos, trending, home feed 🚧 Planned
Community posts 🚧 Planned
Async (asyncio) API 🚧 Planned

ytscrape vs. the alternatives

ytscrape YouTube Data API yt-dlp Browser automation
API key required
Daily quota
Browser / driver needed
Search
Video metadata
Comments + replies ✅ (quota)
Typed Python models
Downloads media
Install size tiny medium large huge

Rule of thumb: use yt-dlp when you need to download media, the official Data API when you need guaranteed, ToS-blessed access, and ytscrape when you need fast, key-less access to YouTube metadata and comments from Python.

How it works

ytscrape speaks the private YouTube InnerTube API directly:

  1. It loads youtube.com once and extracts the InnerTube context (API key, client version, visitor data).
  2. It POSTs to youtubei/v1/search, youtubei/v1/player, youtubei/v1/browse and youtubei/v1/next with that context.
  3. Responses are parsed into small, frozen dataclasses; continuation tokens are followed automatically while you iterate.

No browser. No Selenium. No Playwright. Pure HTTP.

Search

from ytscrape import YouTube, SearchFilter

with YouTube() as yt:
    for item in yt.search("lofi", filter=SearchFilter.PLAYLISTS, max_results=10):
        print(item.title, item.url)
Filter Description
SearchFilter.ALL Everything (default)
SearchFilter.VIDEOS Videos only
SearchFilter.CHANNELS Channels only
SearchFilter.PLAYLISTS Playlists only
SearchFilter.SHORTS Shorts only
SearchFilter.MOVIES Movies only

You can also pass the string form: yt.search("python", filter="videos") — no magic EgIQAQ== strings in your code.

Video details

with YouTube() as yt:
    video = yt.video("dQw4w9WgXcQ")  # id
    video = yt.video("https://youtu.be/dQw4w9WgXcQ")  # or any URL

    print(video.title, video.views, video.length_seconds, video.is_live)
    print(video.keywords)

watch?v=, youtu.be/, /shorts/ and /embed/ URLs are all accepted.

Channel details

with YouTube() as yt:
    channel = yt.channel("UCuAXFkgsw1L7xaCfnd5JJOw")  # id
    channel = yt.channel("@RickAstleyYT")  # handle
    channel = yt.channel("https://www.youtube.com/@RickAstleyYT")  # or any URL

    print(channel.title, channel.subscribers, channel.video_count)
    print(channel.country, channel.joined_date, channel.view_count)
    print(channel.photo, channel.banner)
    print(channel.links)  # e.g. {"x": "https://twitter.com/…", "instagram": "…"}
    print(channel.handle, channel.keywords[:5])
    print(channel.url, channel.vanity_url, channel.rss_url)

# Transcripts / captions (manual preferred over auto-generated).
transcript = yt.transcript("dQw4w9WgXcQ", languages=["uk", "en"])
print(transcript.language_code, transcript.is_generated, len(transcript))
for line in transcript[:3]:
    print(f"{line.start:.1f}s  {line.text}")

# Or inspect tracks first:
for track in yt.transcripts("dQw4w9WgXcQ"):
    print(track.language_code, track.is_generated, track.is_translatable)

/channel/UC…, /@handle, /c/… and /user/… URLs are all accepted. Handles are resolved to a UC… id automatically.

Comments

YouTube.comments() returns a lazy CommentThread that transparently pages through every comment:

from ytscrape import YouTube

with YouTube() as yt:
    for comment in yt.comments("https://youtu.be/dQw4w9WgXcQ", max_results=50):
        print(comment.author, "-", comment.text)

Replies. By default only top-level comments are collected. Pass include_replies=True to also collect the replies of every thread; each reply has is_reply=True and is yielded right after the comment it replies to:

for comment in yt.comments(video_url, include_replies=True):
    marker = "  ↳" if comment.is_reply else "-"
    print(f"{marker} {comment.author}: {comment.text}")

Omit max_results to iterate over all comments. When include_replies=True, max_results counts replies too. A ParseError is raised if the video has comments disabled.

Collecting every comment (sort order)

YouTube's default "Top comments" view quietly hides some comments (less relevant ones and "potential spam"), so collecting in that order will appear to skip comments. To get every comment, switch to "Newest first":

from ytscrape import YouTube, CommentSort

with YouTube() as yt:
    # `CommentSort.NEWEST` (or the string "newest") returns every comment.
    for comment in yt.comments(video_url, sort=CommentSort.NEWEST):
        print(comment.author, "-", comment.text)

sort accepts a CommentSort (TOP / NEWEST) or its string value; it defaults to CommentSort.TOP to mirror YouTube's own default view.

Counting what you collected

comments() is a lazy iterator, so the total is known only after the last page:

total = 0
for comment in yt.comments(video_url, sort="newest"):
    total += 1
print(f"Collected {total} comments")

# Or materialise everything at once:
comments = list(yt.comments(video_url, sort="newest"))
print(len(comments))

Language & country

YouTube localises results by interface language (hl) and content region (gl). Configure both when creating YouTube, passing plain ISO codes (or the Language / Country value objects, which wrap the same codes):

from ytscrape import YouTube, Language, Country, Locale

# Just pass raw ISO codes — they are validated and normalised for you.
with YouTube(language="uk", region="UA") as yt:
    for video in yt.search("музика", max_results=10):
        print(video.title, video.url)

# The Language / Country value objects are equivalent (and reusable).
yt = YouTube(language=Language("de"), region=Country("DE"))

# Invalid codes are rejected early (validated with pycountry).
YouTube(region="XX")  # ValueError: Unknown country code 'XX'. ...

# Or pass a ready-made Locale.
yt = YouTube(locale=Locale(language="fr", country="FR"))
print(yt.locale.language.code, yt.locale.country.code)  # fr FR

Language and Country are thin, self-validating value objects around a raw ISO 639-1 / ISO 3166-1 alpha-2 code — there is no hard-coded list, so any valid code works. The chosen locale is sent both in the request context (hl / gl) and as the Accept-Language HTTP header.

Pagination

Pagination is transparent — iterating over the result object automatically loads the next page:

results = yt.search("python")

for item in results:  # loads pages on demand
    print(item.title)

You can also page manually:

results = yt.search("python")
print(len(results.fetch_next_page()))  # explicitly load one more page
print(results.has_more)  # is there another page?

Use max_results to cap how many items you consume.

Data models

All models are frozen dataclasses with full type hints (the package ships py.typed, so mypy/pyright see the types).

Video (search results) — video_id, title, channel, channel_id, duration, views, published, thumbnail, url.

Channelchannel_id, title, handle, subscribers, video_count, thumbnail, url.

Playlistplaylist_id, title, channel, video_count, thumbnail, url.

VideoDetails (from YouTube.video()) — video_id, title, description, channel, channel_id, length_seconds (int), views (int), keywords, is_live, thumbnail, url.

ChannelDetails (from YouTube.channel()) — channel_id, title, description, handle, subscribers, video_count, view_count, keywords, tags, thumbnail / photo (avatar), banner, vanity_url, rss_url, is_family_safe, available_countries, country, joined_date, links (dict like {"x": "…", "instagram": "…"}), url.

Comment:

Field Description
comment_id Unique comment id.
text The comment body.
author Display name of the author.
author_channel_id Channel id of the author (when available).
author_thumbnail URL of the author's avatar.
published Human-readable published time (e.g. 2 days ago).
like_count Like count as an int (None when abbreviated, e.g. 1.2K).
like_count_text Like count as YouTube renders it, keeping abbreviations (e.g. 1.2K, 894).
reply_count Number of replies (top-level comments only).
reply_count_text Reply count as a raw display string (e.g. 64).
heart True if the video's creator hearted the comment.
is_reply True for replies, False for top-level comments.

Error handling

Every error raised by the library derives from YtScraperError:

from ytscrape import YouTube, YtScraperError, RequestError, ParseError

try:
    with YouTube() as yt:
        for comment in yt.comments("dQw4w9WgXcQ"):
            print(comment.text)
except RequestError as exc:  # network / HTTP problem
    print("network error:", exc)
except ParseError as exc:  # unexpected response (e.g. comments disabled)
    print("cannot parse:", exc)
except YtScraperError as exc:  # catch-all
    print("ytscrape failed:", exc)
Exception Raised when
YtScraperError Base class for everything below.
ContextExtractionError The InnerTube context can't be read from YouTube.
RequestError An HTTP request to YouTube fails.
ParseError A response can't be parsed as expected.

Advanced: proxies, retries, custom sessions

YouTube is a thin facade over InnerTubeClient, which accepts your own requests.Session — that is the hook for proxies, retries, custom headers or caching:

import requests
from requests.adapters import HTTPAdapter, Retry
from ytscrape import YouTube, InnerTubeClient

session = requests.Session()
session.proxies = {"https": "http://user:pass@proxy:8080"}
session.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=1)))

client = InnerTubeClient(session=session, timeout=15.0, language="en", region="US")

with YouTube(client=client) as yt:
    print(next(iter(yt.search("python"))).title)

The same injection point makes the library trivial to unit-test: pass a fake session and no network call ever happens.

Command line

# Search
python -m ytscrape search "python tutorial" --filter videos --max 10

# Localised search (Ukrainian interface, Ukrainian region)
python -m ytscrape --language uk --region UA search "музика" --max 10

# Video details
python -m ytscrape video https://www.youtube.com/watch?v=dQw4w9WgXcQ

# Channel details
python -m ytscrape channel @RickAstleyYT
python -m ytscrape transcript dQw4w9WgXcQ --lang en
python -m ytscrape transcript dQw4w9WgXcQ --list

# Collect comments (pass 0 to --max for no limit)
python -m ytscrape comments https://www.youtube.com/watch?v=dQw4w9WgXcQ --max 20

# Collect comments together with their replies
python -m ytscrape comments https://www.youtube.com/watch?v=dQw4w9WgXcQ --replies

# Collect EVERY comment (the default "top" order hides some)
python -m ytscrape comments https://www.youtube.com/watch?v=dQw4w9WgXcQ --sort newest

After installing, a ytscrape console script is also available:

ytscrape search "python" --filter channels --max 5

Examples

Example What it shows
01_search_videos.py Search for videos with SearchFilter.VIDEOS.
02_search_channels_playlists.py Search for channels and playlists.
03_video_details.py Fetch detailed metadata for a single video.
04_pagination.py Iterate transparently or page manually.
05_language_region.py Localise results by language and region.
06_error_handling.py Handle ytscrape exceptions gracefully.
07_video_comments.py Collect all comments (and replies) of a video.
08_channel_details.py Fetch detailed metadata for a single channel.
09_transcript.py List caption tracks and fetch a transcript.

FAQ

Do I need a YouTube Data API key?

No. ytscrape uses the same internal endpoints as the YouTube web app, so there is nothing to register and no quota to manage.

Why are some comments missing?

Because YouTube's default "Top comments" view hides less relevant comments and "potential spam". Pass sort="newest" (or CommentSort.NEWEST) to collect every comment.

Why is like_count None?

YouTube abbreviates large counts (1.2K), which cannot be expressed exactly as an int. The raw string is always available in like_count_text.

Can I use a proxy or rotate IPs?

Yes — inject your own requests.Session into InnerTubeClient. See Advanced.

Will I get rate limited?

There is no published quota, but YouTube may throttle aggressive traffic. Reuse one YouTube instance (it keeps a warm session and context), request only what you need with max_results, and add delays for large crawls.

Does it download videos?

No, and it is not planned — ytscrape is a metadata library. Use yt-dlp for media downloads.

Is scraping YouTube legal?

This library accesses private endpoints, which may conflict with YouTube's Terms of Service. It is provided for research and educational purposes — you are responsible for how you use it.

Is there an async API?

Not yet — it is the top item on the Roadmap. Today you can run the synchronous client in a thread pool (asyncio.to_thread).

Roadmap

  • Async API (asyncio / httpx) alongside the synchronous one.
  • 📺 Channel tabs: videos, Shorts, live, playlists, about.
  • 🎵 Playlist items with transparent pagination.
  • 🔗 Related videos, trending and home feed.
  • 🗒️ Community posts.
  • 📚 A dedicated documentation site with an API reference.

Ideas and votes are welcome in issues.

Contributing

Contributions are very welcome — bug reports, ideas and pull requests alike.

git clone https://github.com/vsmutok/ytscrape
cd ytscrape
uv sync --dev
uv run pytest              # run the test suite
uv run pre-commit run --all-files   # ruff + format + bandit

The project uses Ruff for linting and formatting, bandit for security checks and pre-commit to run them all. See CHANGELOG.md for release notes.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ytscrape-0.1.4.tar.gz (39.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ytscrape-0.1.4-py3-none-any.whl (45.5 kB view details)

Uploaded Python 3

File details

Details for the file ytscrape-0.1.4.tar.gz.

File metadata

  • Download URL: ytscrape-0.1.4.tar.gz
  • Upload date:
  • Size: 39.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ytscrape-0.1.4.tar.gz
Algorithm Hash digest
SHA256 429a9f7fd83bc94c61dacbc08bdd0ccc30df91a9fa816c9b55bbb6878874a2db
MD5 fe24daa5016c9bc62636c10855b93bb7
BLAKE2b-256 d1863090fd4cdf18cc958d014a6a8a10b56e4388818e341bc6b78e44dc135fff

See more details on using hashes here.

Provenance

The following attestation bundles were made for ytscrape-0.1.4.tar.gz:

Publisher: publish.yml on vsmutok/ytscrape

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ytscrape-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: ytscrape-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 45.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ytscrape-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 cedd99d28da150ace74c017cd8ba9552237852a405d51d29fd60c314e5c84339
MD5 91ba03c2a0b24e4bfe32129c82713efd
BLAKE2b-256 6921c24765122b3ac407f24e89310310e27933d0bb99fe118fd5e8cd54b1e2bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for ytscrape-0.1.4-py3-none-any.whl:

Publisher: publish.yml on vsmutok/ytscrape

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page