ThumbnailsGrabber.com

Download a YouTube Thumbnail in Python

Just need the image? Paste a link above to grab every size in one click. Writing a script? The Python below fetches the same file, with the fallback that keeps you off the gray placeholder.

A YouTube thumbnail is a public static image, so downloading one in Python is a single HTTP request, no API key, no scraper, no browser. The catch is the same one every script hits: maxresdefault.jpg, the HD file, does not exist for every video, and on some edges a missing size comes back as a gray 120x90 placeholder at HTTP 200 instead of a clean 404. This page gives a ready-to-paste function that falls back through the sizes and rejects that placeholder, a urllib version with no dependencies, a URL parser for the video ID, and a batch loop. If you only want the file and not the code, the YouTube thumbnail grabber returns every size in one click.

The URL you are fetching

Every snippet below downloads the same predictable address. Take the 11-character video ID from the link and drop it into the pattern:

https://i.ytimg.com/vi/<VIDEO_ID>/maxresdefault.jpg   HD 1280x720, HD uploads only
https://i.ytimg.com/vi/<VIDEO_ID>/sddefault.jpg      640x480, most videos
https://i.ytimg.com/vi/<VIDEO_ID>/hqdefault.jpg      480x360, every video

The img.youtube.com and i.ytimg.com hosts serve the same files. For the complete list of files and when each one exists, see the YouTube thumbnail URL guide.

Download with requests

The short version most tutorials show fetches maxresdefault and checks the status code. That is the bug: for a video without an HD source the placeholder returns 200, so a status check alone saves a 120x90 gray square. Check the size as well. This function tries the three sizes in order and returns the first real one:

import requests

SIZES = ("maxresdefault", "sddefault", "hqdefault")

def thumbnail_url(video_id: str) -> str:
    """Return the URL of the best real thumbnail, skipping the placeholder."""
    for name in SIZES:
        url = f"https://i.ytimg.com/vi/{video_id}/{name}.jpg"
        r = requests.head(url, timeout=5)
        # A missing size 404s; the gray placeholder is 200 but only ~1KB.
        if r.status_code == 200 and int(r.headers.get("content-length", 0)) > 2048:
            return url
    # hqdefault always exists as a real image, so it is a safe last resort.
    return f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg"

def download_thumbnail(video_id: str, path: str = None) -> str:
    path = path or f"{video_id}.jpg"
    r = requests.get(thumbnail_url(video_id), timeout=10)
    r.raise_for_status()
    with open(path, "wb") as f:
        f.write(r.content)
    return path

print(download_thumbnail("dQw4w9WgXcQ"))   # -> dQw4w9WgXcQ.jpg

The HEAD request reads only the headers, so the fallback costs almost nothing before the one real GET. Because maxresdefault at 1280x720 sits at the top of the chain, it is the sharpest a thumbnail comes in; there is no 1080p or 4K file to request, as YouTube thumbnail resolution explains.

Reject the placeholder by pixel width (Pillow)

If you would rather verify the actual image than trust Content-Length, decode it with Pillow and check the width. The real placeholder is exactly 120 pixels wide, so anything wider is genuine:

from io import BytesIO
from PIL import Image
import requests

def is_real(image_bytes: bytes) -> bool:
    return Image.open(BytesIO(image_bytes)).width > 121

r = requests.get("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg", timeout=10)
if r.status_code == 200 and is_real(r.content):
    open("thumb.jpg", "wb").write(r.content)

No dependencies: the urllib version

If you cannot pip install requests, the standard library does the same job. urllib.request fetches the bytes and raises HTTPError on a 404, and the length check skips the placeholder:

from urllib.request import urlopen, Request
from urllib.error import HTTPError

def download(video_id: str, path: str = None) -> str:
    path = path or f"{video_id}.jpg"
    for name in ("maxresdefault", "sddefault", "hqdefault"):
        url = f"https://i.ytimg.com/vi/{video_id}/{name}.jpg"
        try:
            with urlopen(Request(url), timeout=10) as resp:
                data = resp.read()
        except HTTPError:
            continue
        if len(data) > 2048:            # reject the 120x90 placeholder
            with open(path, "wb") as f:
                f.write(data)
            return path
    raise RuntimeError(f"No usable thumbnail for {video_id}")

Get the video ID from any YouTube URL

You rarely start with a bare ID, so parse it out of whatever link you have, watch, youtu.be, Shorts, embed, or live, with urllib.parse:

from urllib.parse import urlparse, parse_qs

def video_id(url_or_id: str) -> str:
    if len(url_or_id) == 11 and "/" not in url_or_id:
        return url_or_id                      # already an ID
    u = urlparse(url_or_id)
    if u.hostname in ("youtu.be",):
        return u.path.lstrip("/")[:11]        # youtu.be/<id>
    if u.path == "/watch":
        return parse_qs(u.query).get("v", [""])[0][:11]
    parts = [p for p in u.path.split("/") if p]   # /shorts/, /embed/, /live/
    return parts[-1][:11] if parts else ""

print(video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ"))  # dQw4w9WgXcQ
print(video_id("https://youtu.be/dQw4w9WgXcQ"))                 # dQw4w9WgXcQ
print(video_id("https://www.youtube.com/shorts/tPEE9ZwTmy0"))   # tPEE9ZwTmy0

Feed the result to download_thumbnail above and you can hand the script a raw link. For every URL shape and the reasons the ID is always 11 characters, see how to find a YouTube thumbnail.

Download many videos at once

Put the IDs in a list and loop, catching errors so one bad ID does not stop the run:

ids = ["dQw4w9WgXcQ", "9bZkp7q19f0", "kJQP7kiw5Fk"]
for vid in ids:
    try:
        print("saved", download_thumbnail(vid))
    except Exception as e:
        print("skip", vid, e)

No script at hand? The bulk YouTube thumbnail downloader takes a list of links in the browser and returns every image with no code. Prefer the shell to Python? The same fetch as curl, wget, and yt-dlp commands covers the terminal route.

When you want yt-dlp to pick the size

If you already depend on yt-dlp, it resolves the best thumbnail a video actually has, so you never guess about maxresdefault:

from yt_dlp import YoutubeDL

with YoutubeDL({"skip_download": True, "quiet": True}) as ydl:
    info = ydl.extract_info("https://www.youtube.com/watch?v=dQw4w9WgXcQ", download=False)

print(info["thumbnail"])          # best single URL
# info["thumbnails"] is the full list, smallest to largest

That is a heavier dependency than requests for one image, so reach for it when yt-dlp is already in the project.

Usage notes

Frequently asked questions

How do I download a YouTube thumbnail in Python?

Send a GET request to https://i.ytimg.com/vi/VIDEO_ID/maxresdefault.jpg with requests or the standard-library urllib and write the bytes to a .jpg file. Because maxresdefault does not exist for every video, fall back to sddefault then hqdefault, and reject any response smaller than about 2KB so you do not save the gray 120x90 placeholder.

Why does my Python script save a tiny gray thumbnail?

For videos without an HD source or custom thumbnail, maxresdefault.jpg either 404s or, on some CDN edges, returns a gray 120x90 placeholder at HTTP 200. A check of status_code == 200 alone passes the placeholder through, which is why the fix is to also check the Content-Length header or the decoded image width and reject anything under about 2KB or 121 pixels wide.

Do I need the YouTube Data API or an API key in Python?

No. Thumbnail files are public static images on i.ytimg.com, so a plain requests or urllib call reaches them with no API key, OAuth token, or quota. You only need the YouTube Data API (videos.list part=snippet returns snippet.thumbnails) when you also want metadata such as the title or view count, or a guaranteed list of which sizes exist.

How do I download a thumbnail without installing requests?

Use urllib from the standard library: urllib.request.urlopen fetches the same URL with no pip install. Read the bytes, check the length is over about 2KB to skip the placeholder, and write them to a file. It is a few lines longer than requests but needs no dependencies.

How do I get the video ID from a URL in Python?

Parse it with urllib.parse.urlparse: read parse_qs(query)['v'] for a watch URL, the path for a youtu.be link, and the last path segment for /shorts/, /embed/, and /live/ URLs. The ID is always the first 11 characters of that value.

How do I download thumbnails for many videos at once in Python?

Put the video IDs in a list and loop over them, calling your download function for each and catching errors so one bad ID does not stop the run. If you would rather not run a script, paste a list of links into the bulk YouTube thumbnail downloader and get every image in one pass.

Can I use yt-dlp from Python instead?

Yes. Import yt_dlp and call YoutubeDL({'skip_download': True}).extract_info(url, download=False); the returned dict has a 'thumbnail' key with the best URL and a 'thumbnails' list of all of them. It resolves the highest thumbnail the video actually has, so you never guess about maxresdefault, at the cost of a heavier dependency than requests.

Prefer not to write any code? Paste any link into the YouTube thumbnail grabber and download every size in one click.