Features Pricing Docs Blog Try Demo Log In Sign Up
Back to Blog

How to take a website screenshot with Python

Learn how to capture website screenshots with Python using three approaches: Selenium, Playwright, and a screenshot API. Step-by-step code, real output screenshots, full-page captures, mobile viewports, and honest comparison of pros and cons for each method.

How to take a website screenshot with Python

Intro

Today I want to walk you through a step-by-step guide on how to capture website screenshots using Python. At first I thought this would take me a good week to put together and test everything properly. Turned out to be much simpler than I expected, especially when using a third-party API. But more on that later.

Python gives you three solid options: Selenium, Playwright, and a screenshot API. Each one covers a different use case and has its own limits. In this guide I'll go through all three, show you working code you can run right now, and explain when each approach makes sense.

What you'll need

Before we start, make sure you have Python 3.8 or higher installed. Check your version:

python --version

If Python is in place, let's create a working directory and a virtual environment:

mkdir python-screenshots
cd python-screenshots
python -m venv venv
source venv/bin/activate  # Linux/macOS
# or venv\Scripts\activate on Windows

The virtual environment keeps your global packages clean. Now we can install libraries and write code.

Method 1: Selenium

Selenium is an old, battle-tested tool for browser automation. If you already use it for testing or scraping, adding screenshots to your existing code is the easiest path. No need to pull in a new dependency — just call one method.

Installation

pip install selenium webdriver-manager

The webdriver-manager package automatically downloads the right Chrome driver. Without it, you'd have to manually download chromedriver and keep its version in sync with your browser version. If you've ever tried doing that by hand, you know it's an adventure on its own.

Basic screenshot

Create a file called selenium_screenshot.py:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
 
# Set up headless mode so the browser doesn't open on screen
options = Options()
options.add_argument('--headless=new')
options.add_argument('--window-size=1280,800')
 
# Create a driver with automatic chromedriver installation
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=options
)
 
# Open the page and take a screenshot
driver.get('https://dev.to')
driver.save_screenshot('dev_to.png')
 
print(f'Saved: dev_to.png')
driver.quit()

Run it:

python selenium_screenshot.py

The terminal shows Saved: dev_to.png, and the file appears in the sidebar on the left. Open it up — there's the DEV Community homepage, captured by the headless browser. The image is 1280x661 pixels, about 309 KB. Notice that Selenium only captured the visible part of the page — whatever fits in the browser window. Content below the fold didn't make it into the screenshot.

Screenshotting a specific element

Sometimes you don't need the whole page — just one particular block. Selenium can capture individual elements too:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
 
options = Options()
options.add_argument('--headless=new')
options.add_argument('--window-size=1280,800')
 
driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=options
)
 
driver.get('https://dev.to')
 
# Find the element by CSS selector and screenshot just that
header = driver.find_element(By.TAG_NAME, 'header')
header.screenshot('dev_to_header.png')
 
print('Saved: header.png')
driver.quit()

Selenium element screenshot — only the header of dev.to captured

The result is a narrow strip spanning the full page width. Just the DEV Community navigation bar, nothing else. This comes in handy when you need to capture a signup form, a product card, or any other specific block on the page.

Where Selenium falls short

Selenium has one notable limitation — it can't do full-page screenshots out of the box. The save_screenshot() method only captures the viewport. There are workarounds involving JavaScript scrolling and image stitching, but they're tedious and unreliable. If you need a full-page screenshot, that's where Playwright comes in.

Method 2: Playwright

Playwright is a more modern alternative from Microsoft. It supports Chrome, Firefox, and Safari through a single API. For screenshots, it beats Selenium for one simple reason — it can do full-page captures by adding literally one line of code.

Installation

pip install playwright
playwright install chromium

The second command downloads Chromium. If you need Firefox or WebKit, you can specify playwright install firefox webkit. For screenshots, Chromium is more than enough.

Basic screenshot

File playwright_screenshot.py:

from playwright.sync_api import sync_playwright
 
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={'width': 1280, 'height': 800})
 
    page.goto('https://news.ycombinator.com')
    page.screenshot(path='hackernews.png')
 
    print('Saved: hackernews.png')
    browser.close()
python playwright_screenshot.py

Playwright screenshot of Hacker News — code in editor, result in VS Code preview, terminal output

You can see the whole process in the terminal: Playwright installed first, then the script ran and printed Saved: hackernews.png. The preview shows a screenshot of Hacker News — orange header, list of posts, everything just like in a browser. The code is noticeably more compact than Selenium — no fiddling with drivers and options. But the real advantage of Playwright is coming up next.

Full-page screenshot

This is what makes Playwright worth picking. One option, full_page=True, and you get a screenshot of the entire page from top to bottom:

from playwright.sync_api import sync_playwright
 
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={'width': 1280, 'height': 800})
 
    page.goto('https://news.ycombinator.com')
    page.screenshot(path='hackernews_full.png', full_page=True)
 
    print('Full-page screenshot saved: hackernews_full.png')
    browser.close()

Playwright full-page screenshot of Hacker News — complete page from top to bottom

The difference is obvious. A regular screenshot gets cut off at the viewport height, while the full-page version captures everything — all 30 posts plus the footer with the search bar. Try doing that in Selenium with a single line — you can't.

Mobile viewport

Let's say you need to see how a site looks on a phone. Playwright lets you emulate specific devices:

from playwright.sync_api import sync_playwright
 
with sync_playwright() as p:
    browser = p.chromium.launch()
 
    # Emulate iPhone 14
    iphone = p.devices['iPhone 14']
    context = browser.new_context(**iphone)
    page = context.new_page()
 
    page.goto('https://news.ycombinator.com')
    page.screenshot(path='hackernews_mobile.png')
 
    print('Mobile screenshot saved: hackernews_mobile.png')
    browser.close()

Playwright mobile screenshot — Hacker News rendered as iPhone 14 viewport

The screenshot shows Hacker News fully adapted to a mobile screen — posts stack in a single column, fonts are larger, navigation is rearranged. Full mobile emulation. Playwright knows the specs of dozens of devices: iPhone, Pixel, iPad, and others. No need to manually figure out viewport widths and user agents.

Waiting for content to load

Another common problem — the screenshot fires before the page has finished loading. This is especially painful with JavaScript-heavy pages. Playwright can wait for a specific element to appear:

from playwright.sync_api import sync_playwright
 
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={'width': 1280, 'height': 800})
 
    page.goto('https://github.com/trending')
    # Wait for the repository list to appear
    page.wait_for_selector('article.Box-row')
    page.screenshot(path='github_trending.png')
 
    print('Screenshot GitHub Trending saved: github_trending.png')
    browser.close()

Playwright screenshot of GitHub Trending page with wait_for_selector — repositories fully loaded

The screenshot shows the GitHub Trending page with the repository list fully loaded. You can see stars, forks, descriptions — all the content is there. Without wait_for_selector you might have gotten a half-empty page with just a spinner. With it, Playwright waits until the target content actually appears in the DOM, then takes the shot.

Method 3: Screenshot API

Selenium and Playwright work great on your local machine. But try running them on a server — that's a different story. Chrome needs to be installed, it pulls in dozens of dependencies, eats RAM, and crashes from time to time. On CI/CD it's a separate headache: Dockerfiles grow, builds slow down.

A screenshot API takes a different approach — you send an HTTP request with a URL, and get a ready screenshot back. No browsers on your side. All the rendering happens remotely.

How it works

I'll show this using ScreenshotRun — an API I built for exactly this kind of task. The free plan gives you 300 requests per month, which is enough to try it out and see if this approach works for you.

First, sign up and copy your API key from the dashboard.

Basic request

File api_screenshot.py:

import requests
 
API_KEY = 'your-api-key-here'
URL = 'https://producthunt.com'
 
response = requests.get(
    'https://screenshotrun.com/api/v1/screenshots/capture',
    headers={
        'Authorization': f'Bearer {API_KEY}',
    },
    params={
        'url': URL,
        'format': 'png',
        'width': 1280,
        'height': 800,
        'response_type': 'image',
    },
    timeout=60,
)
 
if response.status_code == 200:
    with open('producthunt.png', 'wb') as f:
        f.write(response.content)
    print('Screenshot saved: producthunt.png')
else:
    print(f'Error: {response.status_code} — {response.text}')
pip install requests
python api_screenshot.py

No Chromium, no driver. One HTTP request — one file. The requests library is already installed for most Python developers, so there's nothing extra to set up. Note the response_type: 'image' parameter — it tells the API to return the binary image directly, without a JSON wrapper. Also worth setting timeout=60, since rendering heavy pages can take a few seconds.

Full-page screenshot via API

For a full-page screenshot, add one parameter:

import requests
 
API_KEY = 'your-api-key-here'
 
response = requests.get(
    'https://screenshotrun.com/api/v1/screenshots/capture',
    headers={
        'Authorization': f'Bearer {API_KEY}',
    },
    params={
        'url': 'https://github.com/topics',
        'format': 'png',
        'width': 1280,
        'full_page': True,
        'response_type': 'image',
    },
    timeout=60,
)
 
if response.status_code == 200:
    with open('github_topics_full.png', 'wb') as f:
        f.write(response.content)
    print('Full-page screenshot saved: github_topics_full.png')
else:
    print(f'Error: {response.status_code}')

The API scrolls the page itself, waits for lazy-loaded images to appear, and assembles the full snapshot. You don't need to think about scroll logic or timeouts — all of that is handled on the service side.

Mobile screenshot via API

import requests
 
API_KEY = 'your-api-key-here'
 
response = requests.get(
    'https://screenshotrun.com/api/v1/screenshots/capture',
    headers={
        'Authorization': f'Bearer {API_KEY}',
    },
    params={
        'url': 'https://stripe.com',
        'format': 'png',
        'width': 390,
        'height': 844,
        'response_type': 'image',
    },
    timeout=60,
)
 
if response.status_code == 200:
    with open('stripe_mobile.png', 'wb') as f:
        f.write(response.content)
    print('Mobile screenshot saved: stripe_mobile.png')
else:
    print(f'Error: {response.status_code}')

Width 390 and height 844 match the iPhone 14 screen size. You can emulate any mobile viewport the same way — just change the width and height values.

Batch processing multiple URLs

This is where the API really shows its strength. Say you need to screenshot several websites in one go:

import requests
import os
 
API_KEY = 'your-api-key-here'
 
urls = [
    'https://github.com',
    'https://stackoverflow.com',
    'https://dev.to',
    'https://news.ycombinator.com',
    'https://producthunt.com',
]
 
os.makedirs('screenshots', exist_ok=True)
 
for url in urls:
    domain = url.split('//')[1].split('/')[0].replace('.', '_')
    
    response = requests.get(
        'https://screenshotrun.com/api/v1/screenshots/capture',
        headers={
            'Authorization': f'Bearer {API_KEY}',
        },
        params={
            'url': url,
            'format': 'png',
            'width': 1280,
            'height': 800,
            'response_type': 'image',
        },
        timeout=60,
    )
    
    if response.status_code == 200:
        filepath = f'screenshots/{domain}.png'
        with open(filepath, 'wb') as f:
            f.write(response.content)
        print(f'✓ {domain}')
    else:
        print(f'✗ {domain}: {response.status_code}')
 
print(f'\nReady screenshots/')

Batch screenshot results — screenshots folder with multiple PNG files, terminal showing progress

Checkmarks roll through the terminal: ✓ github_com, ✓ stackoverflow_com, ✓ dev_to... PNG files appear in the screenshots/ folder, one per site. You might notice one of the requests returned a 429 (rate limit). The free plan has a limit on concurrent requests. For production use, you can either upgrade to a paid plan or add a pause between requests with time.sleep(1).

Five sites, five files, 35 lines of code. With Selenium you'd have to launch and shut down the browser for each URL. With the API, each request is just a regular HTTP call — no different from calling any other REST API.

Selenium vs Playwright vs API — an honest comparison

I've tried all three approaches. Here's what I think.

Selenium

When it fits: you already have Selenium in your project for testing or scraping. Adding save_screenshot() to existing code takes one line. Installing Selenium just for screenshots isn't worth it.

Pros: the most mature tool out there, with a huge amount of documentation and examples online. Supports all major browsers: Chrome, Firefox, Safari, Edge. You can interact with the page before taking a screenshot — click buttons, fill forms, scroll. Works well with pytest for testing.

Cons: no full-page screenshots out of the box, only viewport. You need to keep chromedriver version in sync with the browser (webdriver-manager helps, but doesn't always solve this). Verbose code even for simple tasks. On a server without a GUI, you need to configure headless mode and install rendering dependencies.

Playwright

When it fits: you're writing a script from scratch and need full-page screenshots, mobile emulation, or smart waiting. For local work and CI/CD with moderate volume, it's the best choice.

Pros: full-page screenshots with a single line — full_page=True. Built-in mobile device emulation with a ready-made device database. Smart waiting — you can wait for a specific element, network idle, or a particular page state. Clean, compact API with less boilerplate compared to Selenium. Same API for Chromium, Firefox, and WebKit.

Cons: Chromium weighs about 400 MB and downloads on first install. Each running browser instance consumes 200-400 MB of RAM. On servers you still need system dependencies for rendering (fonts, libraries). For 100+ screenshots you need to think about parallelism and browser pools.

Screenshot API

When it fits: production, automation, server-side scripts. When you don't want to deal with browser installation and resource management. When you need stability and predictability.

Pros: zero dependencies on your side — just requests. Works on any platform that has an HTTP client: a server, a Lambda function, a Jupyter Notebook. Doesn't consume your server's RAM or CPU. Easy to scale — no difference between 1 screenshot and 1000. The API handles cookie banners, lazy loading, and JavaScript rendering on its own.

Cons: you depend on an external service — if the API is down, no screenshots. The free plan has limits (300 requests/month on ScreenshotRun). A small network delay — each request takes 3-10 seconds instead of 1-3 seconds locally. Paid plans for higher volumes start at $9/month. You can't interact with the page (click, log in) — it's screenshot by URL only.

Bottom line

For one-off screenshots on your own machine — Playwright. For integrating into an existing test infrastructure — Selenium. For server automation and batch processing — Screenshot API.

ScreenshotRun gives you 300 free requests per month — try it and decide for yourself.

Tips that will save you time

A few things I figured out in practice.

Waiting for page load. Pages with heavy JavaScript might not finish rendering by the time the screenshot fires. In Playwright, use wait_for_selector() to wait for a specific element, or wait_for_load_state('networkidle') to wait until all network requests have settled. With the API approach, this is handled on the service side — it waits for full load automatically.

Cookie banners and popups. They constantly cover content in screenshots. In Selenium and Playwright you can dismiss them via JavaScript injection: find the "Accept" button or the entire banner and remove it from the DOM. The ScreenshotRun API can block cookie banners automatically.

Server resources. Each Chrome instance consumes 200-400 MB of RAM. Ten parallel browsers — that's already 2-4 GB just for screenshots. By comparison, the API approach uses approximately 0 MB of your RAM, because rendering happens on someone else's server.

File format. PNG is for pixel-perfect images with no quality loss. JPEG is for when file size matters and you can sacrifice some detail. WebP strikes a good balance between size and quality, though not all image viewers support it yet.

Rate limits. If you're making batch requests through an API, add time.sleep(1) between calls. Most API services limit the number of requests per second, and without a pause some requests will return a 429 error.

So we've covered all three approaches to taking website screenshots with Python — from local libraries with full control to a third-party API with zero setup. I've laid out the pros and cons, provided code examples. All that's left is to try them and pick what works best for your use case.

More from the blog

View all posts
5 Ways Developers Use Screenshot APIs (Beyond Simple Page Captures)

5 Ways Developers Use Screenshot APIs (Beyond Simple Page Captures)

Most people think of screenshot APIs as a simple URL-to-image tool. But developers who've actually integrated one into their stack use it for OG image generation, link preview thumbnails, visual regression testing, compliance archiving, and competitor monitoring. Here are five real scenarios where a screenshot API saves hours of work. Target Keywords: screenshot API use cases, website screenshot API, automated screenshots for developers, screenshot API for OG images, visual regression testing sc

Read more →
How to take a website screenshot with Node.js

How to take a website screenshot with Node.js

Learn how to capture website screenshots in Node.js using Playwright, Puppeteer, and a screenshot API. Step-by-step code examples with real output: full-page captures, custom viewports, mobile emulation, cookie banner handling, and production gotchas.

Read more →
How to take a website screenshot with PHP

How to take a website screenshot with PHP

How to capture website screenshots from PHP using Puppeteer and Node.js -- from installing dependencies to a working PNG. Plus a faster way with an API.

Read more →