
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 ( | Your desktop | The simplest possible screenshot |
PyAutoGUI | Your desktop | Scripts that already control the mouse and keyboard |
| 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 chromiumThe 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.pngTroubleshooting
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
mssneed an X11 session. Log in with an X11 session, or call a Wayland-friendly tool such asgrimthroughsubprocess.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 screenshotsPlaywright says the browser executable does not exist. Run
playwright install chromium. On a bare Linux server or in Docker, useplaywright install --with-deps chromiumto 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
clipoption.
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=Truefor 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!