Search videos, playlists and channels; work with recommendations, suggestions, comments, transcripts, hashtags, Innertube data and stream formats from one compact Python library. The public API stays familiar while transport and resource ownership are centralized for long-running applications.
Published on PyPI as yt-search-python.
pip install yt-search-python
Simple search stays small; pagination is explicit.
from youtubesearchpython import VideosSearch
search = VideosSearch("Arijit Singh", limit=10)
print(search.result())
search.next()
print(search.result())
Open a method to see its signature. Examples and response shapes stay collapsed until you ask for them.
VideosSearch(query, limit=20, language="en", region="US", timeout=None, is_live=None)
Use Search for mixed results or VideosSearch for videos. Call next() for the next page. is_live=True enables live-only search.
from youtubesearchpython import VideosSearch
search = VideosSearch("news", limit=10, is_live=True)
first = search.result()
search.next()
second = search.result(){
"result": [
{
"type": "video",
"id": "VIDEO_ID",
"title": "...",
"publishedTime": "...",
"duration": "...",
"viewCount": {...},
"thumbnails": [...],
"channel": {...},
"link": "https://www.youtube.com/watch?v=..."
}
]
}ChannelsSearch(query, limit=20, language="en", region="US", timeout=None)
PlaylistsSearch(query, limit=20, language="en", region="US", timeout=None)
Both classes support result() and next() like the other search classes.
from youtubesearchpython import ChannelsSearch, PlaylistsSearch
channels = ChannelsSearch("Google Developers", limit=5)
playlists = PlaylistsSearch("Python tutorial", limit=5)
print(channels.result())
print(playlists.result()){
"result": [
{
"type": "channel | playlist",
"id": "...",
"title": "...",
"thumbnails": [...],
"link": "..."
}
]
}CustomSearch(query, searchPreferences, limit=20, language="en", region="US", timeout=None)
Use preference constants such as SearchMode, VideoUploadDateFilter, VideoDurationFilter and VideoSortOrder when constructing a custom search preference.
from youtubesearchpython import CustomSearch, SearchMode
search = CustomSearch(
"Python",
SearchMode.videos,
limit=10,
)
print(search.result()){
"result": [
{
"type": "...",
"id": "...",
"title": "...",
"...": "fields depend on the selected search mode"
}
]
}ChannelSearch(query, browseId, language="en", region="US", searchPreferences="EgZzZWFyY2g%3D", timeout=None)
Useful when the query should be scoped to one channel rather than global YouTube search.
from youtubesearchpython import ChannelSearch
search = ChannelSearch(
"Python",
"UC_x5XG1OV2P6uZZ5FSM9Ttw",
)
print(search.result())
search.next(){
"result": [
{
"id": "VIDEO_OR_PLAYLIST_ID",
"title": "...",
"thumbnails": [...],
"link": "..."
}
]
}Video.getInfo(videoLink, mode=ResultMode.dict, timeout=None, po_token=None, visitor_data=None, proxy=None)
Video.getFormats(videoLink, mode=ResultMode.dict, timeout=None, po_token=None, visitor_data=None, proxy=None)
PO token and visitor data are optional inputs for sessions/clients where YouTube requires them.
from youtubesearchpython import Video
info = Video.getInfo("pnxL4OOzPEc")
formats = Video.getFormats(
"pnxL4OOzPEc",
po_token="TOKEN",
visitor_data="VISITOR_DATA",
){
"title": "...",
"id": "VIDEO_ID",
"duration": {...},
"viewCount": {...},
"channel": {...},
"description": "...",
"thumbnails": [...],
"formats": [...]
}Playlist.get(playlistLink, mode=ResultMode.dict, timeout=None)
Playlist.getInfo(...) · Playlist.getVideos(...) · Playlist(link).getNextVideos()
Regular playlists support continuation pages. Mix/Radio playlists (RD...) use YouTube's native next flow and preserve returned order.
from youtubesearchpython import Playlist
data = Playlist.get("PLAYLIST_ID")
playlist = Playlist("PLAYLIST_ID")
next_page = playlist.getNextVideos()
mix = Playlist.get(
"https://youtube.com/playlist?list=RDpnxL4OOzPEc&playnext=1"
){
"id": "PLAYLIST_ID",
"title": "...",
"channel": {...},
"thumbnails": [...],
"videos": [
{
"id": "VIDEO_ID",
"title": "...",
"duration": "...",
"thumbnails": [...]
}
]
}Recommendations.get(videoId, timeout=None)
The source video is skipped and duplicate IDs are removed without re-sorting YouTube's returned order.
from youtubesearchpython import Recommendations
related = Recommendations.get("pnxL4OOzPEc")
print(related)[
{
"id": "RELATED_VIDEO_ID",
"title": "...",
"thumbnails": [...],
"channel": {...},
"link": "..."
}
]Suggestions.get(query, language="en", region="US", timeout=None, mode=ResultMode.dict)
Suggestions.session(language="en", region="US", timeout=None)
from youtubesearchpython import Suggestions
print(Suggestions.get("Guru Randhawa"))
session = Suggestions.session(language="en", region="US")
print(session.get("Python")){
"result": [
"suggestion one",
"suggestion two",
"..."
]
}Comments.get(videoLink, mode=ResultMode.dict, timeout=None)
Comments(videoLink).getNextComments()
from youtubesearchpython import Comments
first = Comments.get("pnxL4OOzPEc")
comments = Comments("pnxL4OOzPEc")
comments.init()
next_page = comments.getNextComments(){
"result": [
{
"content": "...",
"author": {...},
"publishedTime": "...",
"likeCount": "...",
"replyCount": "..."
}
]
}Transcript.get(videoLink, params=None, mode=ResultMode.dict, timeout=None)
Use params for the desired caption language/track parameters. The optional transcript extra preserves the legacy fallback path.
from youtubesearchpython import Transcript
transcript = Transcript.get(
"pnxL4OOzPEc",
params="en",
)
print(transcript){
"result": [
{
"text": "...",
"start": "...",
"duration": "..."
}
]
}Channel.get(channelId, mode=ResultMode.dict, timeout=None)
Channel(channel_id, request_type=ChannelRequestType.playlists, timeout=None)
from youtubesearchpython import Channel, ChannelRequestType
info = Channel.get("UC_x5XG1OV2P6uZZ5FSM9Ttw")
channel = Channel(
"UC_x5XG1OV2P6uZZ5FSM9Ttw",
request_type=ChannelRequestType.playlists,
)
channel.init()
channel.next(){
"id": "CHANNEL_ID",
"title": "...",
"description": "...",
"thumbnails": [...],
"playlists": [...]
}Hashtag.get(hashtag, mode=ResultMode.dict, limit=60, language="en", region="US", timeout=None)
from youtubesearchpython import Hashtag
music = Hashtag.get(
"music",
limit=10,
language="en",
region="US",
)
print(music){
"result": [
{
"type": "video | short",
"id": "...",
"title": "...",
"thumbnails": [...]
}
]
}StreamURLFetcher(proxy=None, cookies_file=None, po_token=None, visitor_data=None)
get(videoFormats_or_id, itag, po_token=None) · getAll(videoFormats_or_id, po_token=None)
The fetcher does not depend on yt-dlp. Formats that still require encrypted player-JavaScript deciphering are surfaced under unresolved; URLs that retain an n challenge are marked throttled.
PO-token generation and session-aware caching can be handled separately by ytsp-po-token-provider ↗.
from youtubesearchpython import StreamURLFetcher
fetcher = StreamURLFetcher(
po_token="YOUR_PO_TOKEN",
visitor_data="YOUR_VISITOR_DATA",
)
url = fetcher.get("pnxL4OOzPEc", 18)
all_formats = fetcher.getAll("pnxL4OOzPEc"){
"streams": [
{
"itag": 18,
"url": "https://...",
"mimeType": "...",
"throttled": false
}
],
"unresolved": [
{
"itag": "...",
"reason": "signature deciphering required"
}
]
}Search classes load the first page on the first awaited next(). Content methods such as Video.getInfo, Playlist.get, Recommendations.get and StreamURLFetcher.getAll are awaitable in the future namespace.
import asyncio
from youtubesearchpython.future import VideosSearch
async def main():
search = VideosSearch("Arijit Singh", limit=10)
first = await search.next()
second = await search.next()
print(first, second)
asyncio.run(main())Same logical result structures as the synchronous API. The difference is lifecycle: network operations are awaited.
ResultMode.dict · ResultMode.json
SearchMode.videos · channels · playlists · livestreams
VideoUploadDateFilter.lastHour · today · thisWeek · thisMonth · thisYear
VideoDurationFilter.short · long
VideoSortOrder.relevance · uploadDate · viewCount · rating
ChannelRequestType.info · playlists
from youtubesearchpython import (
ResultMode,
SearchMode,
VideoSortOrder,
)
# Result mode is accepted by content APIs.
# Search/filter constants expose the preference values used by YouTube search.
print(ResultMode.dict)
print(SearchMode.videos)
print(VideoSortOrder.relevance)ResultMode.dict -> Python dictionaries/lists ResultMode.json -> JSON string output where supported
close_clients() · await aclose_clients()
Normal sync applications require no explicit shutdown call. Async clients are owned by their event loops and close when their loop shuts down gracefully. Explicit teardown remains available for tests or unusual lifecycle control.
# Optional forced teardown only
from youtubesearchpython import close_clients
close_clients()
# Async:
from youtubesearchpython.future import aclose_clients
await aclose_clients()Transport ownership is centralized instead of letting independent components accumulate their own client pools.
One canonical HTTP layer reduces duplicate client creation and makes resource ownership predictable.
Async clients are associated with their owning event loop and clean up with normal loop shutdown.
Python 3.9+, runtime-tested on Python 3.13.5 and audited against Python 3.14 asyncio removals/deprecations.
Choose the right Telegram destination. The labels are clickable; raw URLs stay out of the interface.