How to Take Screenshots in Python: Full Screen, Full Page, and Custom Settings

A practical guide with copy-and-paste code for desktop captures, multi-monitor setups, and complete full-page website screenshots.

·
8 min read
·
Human-written

AI tooling disclosure

The author reports no meaningful generative-AI assistance.

How to Take Screenshots in Python: Full Screen, Full Page, and Custom Settings

Taking one screenshot by hand is easy. Taking hundreds on a schedule, capturing an entire web page including everything below the fold, or grabbing one exact region of a monitor is where Python shines. In this post you will learn several ways to do it, from a three-line script to a reusable command-line tool, and when to pick each one.

Every snippet below is complete, so you can paste it into a .py file and run it.

Which tool should you use?

Python has no built-in screenshot module, so the right choice depends on what you want to capture.

Library

Captures

Best for

Pillow (ImageGrab)

Your desktop

The simplest possible screenshot

PyAutoGUI

Your desktop

Scripts that already control the mouse and keyboard

mss

Your desktop, every monitor

Speed, multiple monitors, repeated captures

Playwright

Web pages in a headless browser

Full-page screenshots, device emulation, dark mode

Selenium

Web pages in a browser

Projects that already use Selenium

Rule of thumb: for your own screen use mss or Pillow, and for a website use Playwright.

Setup

You need Python 3.9 or newer. Install everything used in this post:

pip install pillow mss pyautogui playwright
playwright install chromium

The second command downloads the Chromium build that Playwright controls. You only need it for the website sections.

1. The three-line screenshot with Pillow

Pillow’s ImageGrab module is the quickest way to capture your screen:

from PIL import ImageGrab

screenshot = ImageGrab.grab()
screenshot.save('screenshot.png')
print('Saved', screenshot.size)

This captures the primary screen and writes it as a PNG. Pillow picks the file format from the extension, so screenshot.jpg or screenshot.webp works too.

Capture only a region

The bbox argument takes (left, top, right, bottom) in pixels:

from PIL import ImageGrab

region = ImageGrab.grab(bbox=(100, 100, 900, 700))
region.save('region.png')

PyAutoGUI does the same job, but its region is (left, top, width, height) instead. Mixing the two up is a classic source of wrongly cropped images:

import pyautogui

pyautogui.screenshot('pyautogui_full.png')
pyautogui.screenshot('pyautogui_region.png', region=(100, 100, 800, 600))

2. Multi-monitor and fast captures with mss

If you have more than one display, or you need to take many screenshots quickly, mss is the better choice. It treats your displays as a numbered list: index 0 is all monitors combined, index 1 is the primary monitor, index 2 is the second one, and so on.

import mss
import mss.tools

with mss.mss() as sct:
    print(sct.monitors)  # inspect the available monitors

    # Primary monitor
    shot = sct.grab(sct.monitors[1])
    mss.tools.to_png(shot.rgb, shot.size, output='primary.png')

    # Every monitor stitched into one image
    shot = sct.grab(sct.monitors[0])
    mss.tools.to_png(shot.rgb, shot.size, output='all_monitors.png')

    # Each monitor in its own file
    for number, monitor in enumerate(sct.monitors[1:], start=1):
        shot = sct.grab(monitor)
        mss.tools.to_png(shot.rgb, shot.size, output=f'monitor_{number}.png')

Capture a custom region with mss

Here a region is a dictionary, which is much harder to mix up than a tuple:

import mss
import mss.tools

region = {'left': 100, 'top': 100, 'width': 800, 'height': 600}

with mss.mss() as sct:
    shot = sct.grab(region)
    mss.tools.to_png(shot.rgb, shot.size, output='mss_region.png')

3. A reusable function with delay, format, quality, and resizing

In real projects you want options: wait a few seconds so you can switch windows, save as JPEG or WebP with a quality setting, shrink the image, and avoid overwriting old files. This helper uses mss for the capture and Pillow for the processing.

import time
from datetime import datetime
from pathlib import Path

import mss
from PIL import Image


def take_screenshot(
    output_dir='screenshots',
    delay=0,
    monitor_index=1,
    region=None,
    image_format='png',
    quality=90,
    scale=1.0,
):
    '''Capture the screen and return the path of the saved file.

    delay          seconds to wait before capturing
    monitor_index  0 = all monitors, 1 = primary, 2 = second monitor, ...
    region         optional dict: {'left': 0, 'top': 0, 'width': 800, 'height': 600}
    image_format   'png', 'jpg' or 'webp'
    quality        1-100, used for jpg and webp
    scale          1.0 = original size, 0.5 = half size
    '''
    if delay:
        time.sleep(delay)

    folder = Path(output_dir)
    folder.mkdir(parents=True, exist_ok=True)

    with mss.mss() as sct:
        target = region if region else sct.monitors[monitor_index]
        raw = sct.grab(target)
        img = Image.frombytes('RGB', raw.size, raw.bgra, 'raw', 'BGRX')

    if scale != 1.0:
        new_size = (int(img.width * scale), int(img.height * scale))
        img = img.resize(new_size, Image.Resampling.LANCZOS)

    fmt = image_format.upper()
    if fmt == 'JPG':
        fmt = 'JPEG'
    extension = 'jpg' if fmt == 'JPEG' else fmt.lower()

    path = folder / f'screenshot_{datetime.now():%Y%m%d_%H%M%S}.{extension}'
    save_options = {'quality': quality} if fmt in ('JPEG', 'WEBP') else {}
    img.save(path, format=fmt, **save_options)
    return path


if __name__ == '__main__':
    # Primary monitor, PNG, timestamped file name
    print(take_screenshot())

    # Wait 5 seconds, then save a half-size JPEG at quality 80
    print(take_screenshot(delay=5, image_format='jpg', quality=80, scale=0.5))

    # Only the top-left 800x600 area, as WebP
    print(take_screenshot(region={'left': 0, 'top': 0, 'width': 800, 'height': 600},
                          image_format='webp'))

    # All monitors stitched together
    print(take_screenshot(monitor_index=0))

A few things worth noticing:

  • The timestamp in the file name means you never overwrite an earlier screenshot.

  • Image.frombytes(..., 'BGRX') converts mss’s raw pixel data into a normal Pillow image, so you can use any Pillow feature on it (cropping, filters, annotations).

  • JPEG has no transparency, which is why the image is converted to plain RGB first.

4. Take a screenshot every few seconds

Creating one mss object and reusing it is faster than creating a new one for every shot, which makes it ideal for timelapses or monitoring a long-running job:

import time
from pathlib import Path

import mss
import mss.tools


def capture_every(seconds, count, folder='timelapse'):
    Path(folder).mkdir(exist_ok=True)
    with mss.mss() as sct:
        for i in range(1, count + 1):
            shot = sct.grab(sct.monitors[1])
            mss.tools.to_png(shot.rgb, shot.size, output=f'{folder}/shot_{i:03d}.png')
            print(f'Saved shot {i}/{count}')
            if i < count:
                time.sleep(seconds)


capture_every(seconds=5, count=6)

This saves six screenshots, five seconds apart, as shot_001.png, shot_002.png, and so on.

5. Full-page screenshots of a website with Playwright

Desktop tools can only capture what is currently on your screen. If you need a complete screenshot of a web page, including everything you would normally scroll to see, use a headless browser. Playwright makes this a one-line option.

Viewport vs. full page

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://example.com')

    page.screenshot(path='viewport.png')                    # only the visible area
    page.screenshot(path='full_page.png', full_page=True)   # the entire scrollable page

    browser.close()

Useful settings

Most options live on the browser context (how the page is rendered) or on page.screenshot() (how the image is saved):

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context(
        viewport={'width': 1440, 'height': 900},  # window size
        device_scale_factor=2,                     # 2x pixels for a sharp, retina-style image
        color_scheme='dark',                       # ask the site for its dark theme
        locale='en-US',
    )
    page = context.new_page()
    page.goto('https://example.com', wait_until='load')

    # JPEG with a quality setting
    page.screenshot(path='page.jpg', type='jpeg', quality=80, full_page=True)

    # Transparent background (PNG only; only matters if the page has no background color)
    page.screenshot(path='transparent.png', omit_background=True)

    # Crop to a specific area of the page
    page.screenshot(path='clip.png', clip={'x': 0, 'y': 0, 'width': 800, 'height': 400})

    # Screenshot of a single element
    page.locator('h1').screenshot(path='heading.png')

    browser.close()

Emulate a phone

Set a phone-sized viewport and the mobile flags to see the mobile layout of a site:

from playwright.sync_api import sync_playwright

IPHONE_UA = ('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) '
             'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1')

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context(
        viewport={'width': 390, 'height': 844},
        device_scale_factor=3,
        is_mobile=True,
        has_touch=True,
        user_agent=IPHONE_UA,
    )
    page = context.new_page()
    page.goto('https://example.com')
    page.screenshot(path='mobile_full.png', full_page=True)
    browser.close()

The lazy-loading problem

Many modern sites only load images and sections when you scroll to them. A full-page screenshot taken immediately can show blank gaps. The fix is to scroll through the page first, then capture:

PAGE_HEIGHT_JS = 'Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)'


def scroll_to_bottom(page, step=600, pause_ms=150, max_steps=60):
    for i in range(max_steps):
        position = i * step
        if position >= page.evaluate(PAGE_HEIGHT_JS):
            break
        page.evaluate(f'window.scrollTo(0, {position})')
        page.wait_for_timeout(pause_ms)
    page.evaluate('window.scrollTo(0, 0)')

The max_steps limit stops the script from scrolling forever on infinite-scroll pages.

Putting it all together: a command-line screenshot tool

Save this as webshot.py. It combines everything above into one tool with sensible defaults:

'''webshot.py - capture a full-page screenshot of any URL.

Examples:
    python webshot.py https://example.com
    python webshot.py https://example.com -o shot.jpg --width 1920 --scale 2 --dark
    python webshot.py https://example.com --mobile --delay 2
    python webshot.py https://example.com --viewport-only
'''
import argparse
from pathlib import Path
from urllib.parse import urlparse

from playwright.sync_api import TimeoutError as PlaywrightTimeout
from playwright.sync_api import sync_playwright

MOBILE_UA = (
    'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) '
    'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
)
PAGE_HEIGHT_JS = 'Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)'


def scroll_to_bottom(page, step=600, pause_ms=150, max_steps=60):
    '''Scroll down in steps so lazy-loaded content has a chance to load.'''
    for i in range(max_steps):
        position = i * step
        if position >= page.evaluate(PAGE_HEIGHT_JS):
            break
        page.evaluate(f'window.scrollTo(0, {position})')
        page.wait_for_timeout(pause_ms)
    page.evaluate('window.scrollTo(0, 0)')


def capture(url, output, width=1366, height=768, scale=1.0, dark=False,
            mobile=False, full_page=True, delay=0.0, quality=85, timeout_ms=30000):
    suffix = Path(output).suffix.lower()
    image_type = 'jpeg' if suffix in ('.jpg', '.jpeg') else 'png'

    context_options = {
        'viewport': {'width': width, 'height': height},
        'device_scale_factor': scale,
        'color_scheme': 'dark' if dark else 'light',
    }
    if mobile:
        context_options.update(
            viewport={'width': 390, 'height': 844},
            is_mobile=True,
            has_touch=True,
            user_agent=MOBILE_UA,
        )

    with sync_playwright() as p:
        browser = p.chromium.launch()
        try:
            page = browser.new_context(**context_options).new_page()
            page.goto(url, wait_until='load', timeout=timeout_ms)
            try:
                page.wait_for_load_state('networkidle', timeout=5000)
            except PlaywrightTimeout:
                pass  # some sites never go fully idle, and that is fine

            if full_page:
                scroll_to_bottom(page)
            if delay:
                page.wait_for_timeout(delay * 1000)

            options = {'path': str(output), 'full_page': full_page, 'type': image_type}
            if image_type == 'jpeg':
                options['quality'] = quality
            page.screenshot(**options)
        finally:
            browser.close()
    return output


def main():
    parser = argparse.ArgumentParser(description='Take a screenshot of a web page.')
    parser.add_argument('url')
    parser.add_argument('-o', '--output', help='Output file (.png or .jpg). Default: the domain name + .png')
    parser.add_argument('--width', type=int, default=1366)
    parser.add_argument('--height', type=int, default=768)
    parser.add_argument('--scale', type=float, default=1.0, help='Device scale factor (2 = retina)')
    parser.add_argument('--quality', type=int, default=85, help='JPEG quality, 1-100')
    parser.add_argument('--delay', type=float, default=0.0, help='Extra seconds to wait before capturing')
    parser.add_argument('--dark', action='store_true', help='Request the dark color scheme')
    parser.add_argument('--mobile', action='store_true', help='Emulate a phone')
    parser.add_argument('--viewport-only', action='store_true', help='Capture only the visible area')
    args = parser.parse_args()

    url = args.url if args.url.startswith(('http://', 'https://')) else 'https://' + args.url
    output = args.output or urlparse(url).netloc.replace(':', '_') + '.png'

    capture(
        url, output,
        width=args.width, height=args.height, scale=args.scale,
        dark=args.dark, mobile=args.mobile, full_page=not args.viewport_only,
        delay=args.delay, quality=args.quality,
    )
    print('Saved', output)


if __name__ == '__main__':
    main()

Run it from the terminal:

python webshot.py example.com
python webshot.py example.com -o retina_dark.jpg --scale 2 --dark
python webshot.py example.com --mobile -o phone.png

Troubleshooting

  • The screenshot is black or empty on macOS. Give your terminal or IDE permission under System Settings, Privacy & Security, Screen Recording, then restart it.

  • It fails on Linux with Wayland. Pillow, PyAutoGUI, and mss need an X11 session. Log in with an X11 session, or call a Wayland-friendly tool such as grim through subprocess.

  • Windows crops the image or captures the wrong area. Display scaling (125% or 150%) can confuse coordinates. Make the process DPI-aware before capturing:

    import ctypes
    ctypes.windll.shcore.SetProcessDpiAwareness(2)  # Windows only; call before taking screenshots
  • Playwright says the browser executable does not exist. Run playwright install chromium. On a bare Linux server or in Docker, use playwright install --with-deps chromium to also install system libraries.

  • Playwright errors inside Jupyter. The sync API conflicts with the notebook’s event loop. Run the code as a normal script, or switch to playwright.async_api.

  • Very tall pages come out cut off or fail. Browsers limit image height (around 16,000 pixels in Chromium). For extreme pages, capture in sections using the clip option.

Wrapping up

Here is the short version:

  • Use Pillow or PyAutoGUI for a quick one-off desktop screenshot.

  • Use mss for multiple monitors, speed, and repeated captures.

  • Use Playwright with full_page=True for complete website screenshots, and add scrolling, scale, dark mode, or mobile emulation as needed.

Once you can capture the screen from code, you can plug it into test reports, monitoring dashboards, documentation generators, or visual regression checks. Pick the snippet that fits your case, tweak the settings, and automate the boring part.

Comments(0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Shobit Singh

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Shobit Singh's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.