Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

To capture a particular macOS window without bringing it to the front, use Apple’s ScreenCaptureKit through PyObjC. Ask ScreenCaptureKit for shareable windows, choose the target window, create a window-specific content filter, and capture that filter. The target can be behind another window or offscreen; that is different from running your Python process in the background. macOS must grant your terminal or app Screen Recording permission before any pixels are returned.

This approach follows Apple’s current window-capture framework rather than the deprecated CGWindowListCreateImage API. The API is available on macOS 12.3 and later through PyObjC’s ScreenCaptureKit bindings, although exact method availability can vary by macOS and PyObjC release.

What “background app” means

There are two cases that are often mixed together:

  • A window behind another window or offscreen: ScreenCaptureKit can select that shareable window directly. It does not require you to activate the app.
  • Your capture process is backgrounded: Apple documents separate background-execution configuration for the capturing application. A Python script launched from Terminal is not automatically equivalent to a macOS app with those modes.

The code below addresses the first case: selecting one window, even when it is not the frontmost visible desktop content.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prerequisites and permission

  1. Use a Mac running macOS 12.3 or newer. ScreenCaptureKit’s Python bindings are documented as new in PyObjC for macOS 12.3.
  2. Install PyObjC in the same Python environment that will run the script: python3 -m pip install pyobjc-framework-ScreenCaptureKit pyobjc-framework-Quartz pyobjc-framework-ImageIO.
  3. Open System Settings → Privacy & Security → Screen & System Audio Recording (the label can vary slightly by macOS release) and enable the application that launches Python, commonly Terminal, iTerm, or your IDE.
  4. Run the script once, grant access when prompted, then restart the launching app if capture still returns no content. Apple’s macOS sample explicitly says the sample must be restarted after permission is granted.

Do not install Apple’s separate CoreGraphics Python package alongside PyObjC’s Quartz bindings. PyObjC’s notes recommend importing Quartz from PyObjC.

Enumerate windows and choose the target

ScreenCaptureKit exposes shareable displays, applications, and windows. The first practical step is to list windows so you can identify the target by owner name, title, and window ID. This script performs that enumeration and writes a JSON-like listing to standard output.

#!/usr/bin/env python3
import ScreenCaptureKit as SCK
import Foundation

class ContentDelegate(Foundation.NSObject):
    def __init__(self):
        super().__init__()
        self.done = Foundation.NSConditionLock.alloc().initWithCondition_(False)
        self.content = None
        self.error = None

    def contentWithShareableContent_completionHandler_(self, content, error):
        self.content, self.error = content, error
        self.done.lock()
        self.done.condition = True
        self.done.unlock()

delegate = ContentDelegate()
SCK.SCShareableContent.getShareableContentWithCompletionHandler_(
    delegate.contentWithShareableContent_completionHandler_
)
delegate.done.lockWhenCondition_(True)
delegate.done.unlock()

if delegate.error:
    raise RuntimeError(delegate.error)

for window in delegate.content.windows():
    owner = window.owningApplication()
    print({
        "window_id": window.windowID(),
        "title": window.title() or "",
        "owner": owner.applicationName() if owner else "",
        "bundle_id": owner.bundleIdentifier() if owner else "",
        "active": bool(window.isActive()),
        "frame": str(window.frame()),
    })

PyObjC method names are generated from Objective-C selectors. If your installed release exposes a slightly different spelling, inspect it with dir(SCK.SCShareableContent) and consult the PyObjC ScreenCaptureKit notes. The important result is a SCWindow object, not a desktop-wide image.

Capture one selected window

Once you have a window object, construct a content filter for that window and pass it to ScreenCaptureKit’s still-image manager. The following example shows the complete control flow. It intentionally checks for API availability because Apple and PyObjC have added still-image conveniences across macOS releases.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#!/usr/bin/env python3
import sys
import ScreenCaptureKit as SCK
import Foundation
import Quartz
import ImageIO

TARGET_OWNER = "TextEdit"       # change this
TARGET_TITLE = ""               # optional exact title; leave empty to ignore
OUTPUT = "background-window.png"

class Waiter(Foundation.NSObject):
    def __init__(self):
        super().__init__()
        self.condition = Foundation.NSConditionLock.alloc().initWithCondition_(False)
        self.value = None
        self.error = None
    def handler_(self, value, error):
        self.value, self.error = value, error
        self.condition.lock(); self.condition.condition = True; self.condition.unlock()

def wait_for(call):
    w = Waiter(); call(w.handler_)
    w.condition.lockWhenCondition_(True); w.condition.unlock()
    if w.error: raise RuntimeError(w.error)
    return w.value

content = wait_for(SCK.SCShareableContent.getShareableContentWithCompletionHandler_)
chosen = None
for window in content.windows():
    app = window.owningApplication()
    owner = app.applicationName() if app else ""
    title = window.title() or ""
    if owner == TARGET_OWNER and (not TARGET_TITLE or title == TARGET_TITLE):
        chosen = window; break
if chosen is None:
    raise SystemExit("No matching shareable window; check the owner/title listing.")

filter_ = SCK.SCContentFilter.alloc().initWithDesktopIndependentWindow_(chosen)
config = SCK.SCStreamConfiguration.alloc().init()
config.width = max(1, int(chosen.frame().size.width))
config.height = max(1, int(chosen.frame().size.height))
config.pixelFormat =  'BGRA'

manager = getattr(SCK, "SCScreenshotManager", None)
if manager is None:
    raise RuntimeError("This macOS/PyObjC version has no SCScreenshotManager; use a stream output instead.")
shot = wait_for(lambda cb: manager.captureImageWithFilter_configuration_completionHandler_(filter_, config, cb))
if shot is None:
    raise RuntimeError("Capture returned no image. Check Screen Recording permission and app restrictions.")
url = Foundation.NSURL.fileURLWithPath_(OUTPUT)
dest = ImageIO.CGImageDestinationCreateWithURL(url, "public.png", 1, None)
if not dest:
    raise RuntimeError("Could not create PNG destination")
ImageIO.CGImageDestinationAddImage(dest, shot, None)
if not ImageIO.CGImageDestinationFinalize(dest):
    raise RuntimeError("Could not write PNG")
print(f"Saved {OUTPUT}")

Objective-C selector exposure differs between PyObjC releases; in particular, the still-image manager may be unavailable on older systems. If the final capture selector is not present, use an SCStream with a video-output delegate and save the first sample buffer. The selection logic remains the same: obtain SCWindow, build SCContentFilter for that window, then configure the stream. Treat this as an API integration point to verify against the installed bindings rather than a promise that every macOS/PyObjC combination has identical names.

Window-specific details that affect results

Offscreen and inactive windows

Apple’s SCWindow.active reference allows a window to be streamed even when it is offscreen. “Inactive” does not mean “uncapturable,” but the target must still appear in ScreenCaptureKit’s shareable-content list.

Protected or unsupported surfaces

Capture is controlled by the target application. Apple gives Apple TV as an example of an app that may not allow screenshots of its windows. DRM-protected video, secure fields, minimized windows, and transient surfaces can therefore produce black, empty, or unavailable output.

Permission is per launching app

Granting access to Terminal does not necessarily grant it to an IDE, launch agent, or packaged Python application. Enable the actual process shown in System Settings, then restart it when required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why not use the older Quartz recipe?

Many older Python examples call CGWindowListCreateImage through Quartz. Apple marks that API deprecated. macOS Sequoia 15 release notes warn that deprecated capture APIs such as CGDisplayStream and CGWindowListCreateImage can trigger alerts about potential detailed collection of user information. Use ScreenCaptureKit for new window-oriented work; keep Quartz code only for maintaining an existing legacy tool while you plan a migration.

Quartz Window Services remains useful for discovering window metadata, but metadata discovery is not the same as obtaining pixels. Do not assume a window ID from an old Quartz snippet can be passed directly into a modern capture call without creating the corresponding ScreenCaptureKit object.

Troubleshooting

No windows are listed

Check Screen Recording permission for the process that launched Python, confirm that the target is a normal app window, and restart that process after granting permission. Verify the owner and title strings rather than relying on a localized title.

“No matching shareable window”

Print every owner, title, and ID, then adjust TARGET_OWNER. A document title can change while the app name remains stable. Avoid selecting by index because opening or closing a window changes ordering.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Black or transparent output

The app may protect its content, the window may be minimized or transient, or the capture configuration may not match the selected frame size. Try a normal unprotected window and derive width and height from window.frame().

Attribute or selector errors

PyObjC bindings track the installed SDK and can expose different selector spellings. Compare dir(ScreenCaptureKit) and the binding notes with your macOS version. Do not mix examples written for a newer SDK with an older runtime.

The script works only when visible

Confirm that you selected a window filter, not a display filter, and distinguish the target window’s location from your Python process’s execution state. A background execution requirement belongs to the app hosting the capture code, not to the window-selection API.

Reliability, performance, and operational choices

  • Use stable identifiers: Match owner, bundle identifier, and title; record the window ID for diagnostics, but expect IDs to change after an app restart.
  • Capture only what you need: A single-window filter avoids accidentally recording the entire desktop and reduces downstream image handling.
  • Retry discovery, not permission: A newly opened window may not appear immediately. Poll shareable content briefly, but surface permission errors instead of retrying forever.
  • Plan for OS differences: ScreenCaptureKit is the current framework, yet Python selector availability depends on macOS and PyObjC versions. Pin and test the versions used in deployment.
  • Do not promise universal support: Content restrictions are imposed by each app, and Apple documents exceptions.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If what you actually need is a screenshot of a public web page rather than a native macOS window, ScreenshotNeo avoids local browser automation. It accepts a URL and returns PNG, JPEG, WebP, or PDF; cookie-consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

One request is enough:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for all options. A Python client is:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Reference links

Frequently Asked Questions

Can Python capture a minimized macOS window?

ScreenCaptureKit can stream some offscreen windows, but a minimized, protected, or otherwise unsupported surface may return no usable pixels. Test the specific app and state.

Does Screen Recording permission expose every app?

No. Permission allows capture requests, while each app can still restrict its content; Apple cites Apple TV as an example.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is this the same as taking a screenshot of the desktop?

No. A desktop capture records visible display content. A window filter targets one shareable window independently of what is in front.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.