PyWinCtl

CI PyPI version Documentation Status Downloads Stars License

Cross-platform window management for Python. Discover, control, and monitor any open window on your desktop — across Windows, macOS, and Linux — with a single, unified API.

With PyWinCtl you can list open windows, retrieve their properties, move and resize them, minimize, maximize, restore, activate, close, and even track window state changes in real time.

PyWinCtl uses native backends under the hood: Win32 API on Windows, Apple Script on macOS, and EWMH/Xlib on Linux; making it an ideal solution for desktop automation, screen recording, UI testing, window monitoring or tiling, kiosks, overlays, and multi-monitor workflows.

Sincere thanks to MestreLion, super-ibby, Avasam, macdeport, holychowders, and all other contributors (see AUTHORS.txt) for their help, feedback, and moral support.


What is this for?

If you’ve ever needed to do any of the following from a Python script, this library is for you:

  • Find a window — get the active window or find any other window by its title, getting an object to query or modify its properties

  • Move or resize a window — position a browser at exactly (0, 0) before taking a screenshot, or snap two windows side-by-side automatically

  • Bring a window to the front — activate a specific app after launching it via subprocess

  • Get notified when a window closes, moves, or changes title — react in real time from a background thread

  • Automate GUI workflows — launch an app, wait for its window, interact with its menu, and close it programmatically

  • Manage a multi-window test harness — enumerate all open windows, find ones by title or PID, check their state

  • Build a screen capture tool — get the exact position and size of a window to pass to mss, PIL, or OpenCV

  • Control your own app’s windows — manage Tkinter/Qt/wx window geometry or state from outside the main loop

import pywinctl as pwc

# Find a window and take full control
win = pwc.getWindowsWithTitle("Notepad")[0]

win.activate()            # bring to front
win.resizeTo(1280, 720)   # set exact size
win.moveTo(0, 0)          # snap to top-left corner
win.alwaysOnTop(True)     # pin it above everything else

# Read live properties
print(win.title, win.size, win.isMaximized, win.isAlive)

Real-world use cases

Screen capture with exact client frame coordinates (skip borders and title bar)

import pywinctl as pwc
import mss

win = pwc.getWindowsWithTitle("My App")[0]
frame = win.getClientFrame()
box = {"left": frame.left, "top": frame.top, "width": frame.right - frame.left, "height": frame.bottom - frame.top}
with mss.mss() as sct:
    win.activate()
    screenshot = sct.grab(box)

Wait for a launched app to appear

import subprocess, time, pywinctl as pwc

subprocess.Popen("notepad")
win = None
while not win:
    time.sleep(0.2)
    windows = pwc.getWindowsWithTitle("Notepad")
    if windows:
        win = windows[0]
win.activate()
win.resizeTo(800, 600)

React when a window closes

import pywinctl as pwc

def on_closed(is_alive):
    print("Window gone!")

win = pwc.getActiveWindow()
win.watchdog.start(isAliveCB=on_closed)

Tile two windows side by side, regardless of which monitor they are on

import pywinctl as pwc
import pymonctl as pmc

def get_coordinates(win):
   window_monitor = win.getMonitor()[0]
   monitor_info = pmc.getAllMonitorsDict()[window_monitor]
   return monitor_info["position"], monitor_info["size"]
   
wins = pwc.getWindowsWithTitle("brave|notepad", None, pwc.Re.MATCH, pwc.Re.IGNORECASE)
brave, notepad = wins

pos, size = get_coordinates(brave)

brave.moveTo(pos.x, pos.y)
brave.resizeTo(size.width // 2, size.height)
notepad.moveTo(size.width // 2, pos.y)
notepad.resizeTo(size.width // 2, size.height)

Ecosystem

PyWinCtl is based on these other libraries, which offer a rich set of additional, useful features:

  • PyMonCtl → monitor management (especially for multi-monitor awareness)

  • PyWinBox → geometry utilities (similar to PyGame.Rect object, but enhanced)

  • EWMHlib → Extended Window Manager Hints (EWMH) implementation (X11 only)


Table of contents

  1. Window features

  2. Window change notifications (watchdog)

  3. Menu features

  4. Known gotchas

  5. Install

  6. Support

  7. Using this code

  8. Test


Window features

PyWinCtl exposes three layers of API:

  • Module-level functions — call directly without a Window object (e.g. pwc.getActiveWindow(), pwc.getAllTitles())

  • Window methods — actions on a specific window object (e.g. win.resizeTo(800, 600), win.close())

  • Window properties — readable and writable attributes (e.g. win.title, win.center = (500, 300))

All three layers are available on Windows, Linux, and macOS.

Module-level functions

Window methods

Window properties

getActiveWindow

close

(GET) title

getActiveWindowTitle

minimize

(GET) updatedTitle (MacOSWindow only)

getAllWindows

maximize

(GET) isMaximized

getAllTitles

restore

(GET) isMinimized

getWindowsWithTitle

hide

(GET) isActive

getAllAppsNames

show

(GET) isVisible

getAppsWithName

activate

(GET) isAlive

getAllAppsWindowsTitles

resize / resizeRel

Position / Size (via PyWinBox)

getWindowsAt

resizeTo

(GET/SET) position (x, y)

getTopWindowAt

move / moveRel

(GET/SET) left, top, right, bottom

displayWindowsUnderMouse

moveTo

(GET/SET) topleft, topright, bottomleft, bottomright

version

raiseWindow

(GET/SET) midtop, midleft, midbottom, midright

checkPermissions (macOS only)

lowerWindow

(GET/SET) center, centerx, centery

alwaysOnTop

(GET/SET) size (width, height)

alwaysOnBottom

(GET/SET) width, height

sendBehind

(GET/SET) box (x, y, width, height)

acceptInput

(GET/SET) rect (x, y, right, bottom)

getAppName

getHandle

getParent

setParent

getChildren

isParent

isChild

getDisplay

getExtraFrameSize

getClientFrame

getPID

Important macOS notice

macOS restricts controlling windows belonging to other processes. The MacOSWindow() class works via Apple Script, which makes it slower, non-standard, and occasionally tricky (it uses the window name as a reference, which may change or be duplicated). You will likely need to grant permissions under Settings → Security & Privacy → Accessibility. Be aware that some applications have limited or no Apple Script support, so some methods may not work. Calls like getActiveWindowTitle() have observed latencies of 400–500 ms on Apple Silicon — if your use case is latency-sensitive, account for this.

Important Linux notice

The wide variety of Linux distributions, desktop environments, and window managers makes it impossible to test every combination.

PyWinCtl has been tested successfully on these X11 setups: Ubuntu/GNOME, Ubuntu/KDE, Ubuntu/Unity, Mint/Cinnamon, and Raspbian/LXDE. The sendBehind() method does not work correctly on most setups except Mint/Cinnamon and Ubuntu 22.04+.

On Wayland (the default display protocol on Ubuntu 22.04+ and many modern distros), getActiveWindow() and getAllWindows() are unreliable — built-in and “official” applications do not expose their X Window ID, so these calls may fail even with unsafe mode enabled. They may still work for third-party applications such as Chrome, or for your own application’s windows.

WSL2 is not supported — the X server environment WSL2 provides does not expose the information PyWinCtl requires.

If you encounter problems on a configuration not listed above, please open an issue. Contributions for untested configs are very welcome.


Window change notifications

PyWinCtl includes a watchdog — a background thread that monitors a window and fires your callbacks whenever its state changes.

Access it via window.watchdog. The watchdog stops automatically when the window is closed or the main program exits.

Available callbacks:

isAliveCB:        fires when the window is no longer alive
                  passes: False

isActiveCB:       fires when the window gains or loses focus
                  passes: True / False

isVisibleCB:      fires when the window is shown or hidden
                  passes: True / False

isMinimizedCB:    fires when the window is minimized or restored
                  passes: True / False

isMaximizedCB:    fires when the window is maximized or restored
                  passes: True / False

resizedCB:        fires when the window is resized
                  passes: (width, height)

movedCB:          fires when the window is moved
                  passes: (x, y)

changedTitleCB:   fires when the window title changes
                  passes: new title (str)
                  IMPORTANT: on macOS AppScript, if the title changes the watchdog will stop
                  unless setTryToFind(True) is used

changedDisplayCB: fires when the window moves to a different display
                  passes: new display name (str)

Watchdog methods

start

Start watching with the given callbacks

updateCallbacks

Replace active callbacks (pass all desired ones)

updateInterval

Change the polling interval

setTryToFind (macOS only)

Try to locate window after title change

isAlive

Check if the watchdog is still running

stop

Stop the watchdog

Example:

import pywinctl as pwc
import time

def activeCB(active):
    print("NEW ACTIVE STATUS", active)

def movedCB(pos):
    print("NEW POS", pos)

npw = pwc.getActiveWindow()
npw.watchdog.start(isActiveCB=activeCB)
npw.watchdog.setTryToFind(True)
print("Toggle focus and move the active window — press Ctrl-C to quit")
i = 0
while True:
    try:
        if i == 50:
            npw.watchdog.updateCallbacks(isActiveCB=activeCB, movedCB=movedCB)
        if i == 100:
            npw.watchdog.updateInterval(0.1)
            npw.watchdog.setTryToFind(False)
        time.sleep(0.1)
    except KeyboardInterrupt:
        break
    i += 1
npw.watchdog.stop()

Important comments

  • Callback signatures must match their invocation parameters: bool, str, or (int, int).

  • The watchdog is asynchronous — notifications are not immediate. Adjust the polling interval to suit your needs, or read window properties directly (e.g. win.isAlive) for synchronous checks.

  • Move and resize callbacks will fire multiple times as the window transitions between positions.

  • When calling updateCallbacks(), always pass all desired callbacks — any omitted callback (passed as None) will be deactivated.

Important macOS Apple Script notice

  • The Apple Script backend can be slow and resource-intensive.

  • The watchdog identifies the window by its title. If the title changes, the watchdog will consider the window gone and stop — unless setTryToFind(True) is used. This uses a similarity check, so the result is not fully guaranteed.



Known gotchas

These are the most common surprises reported by users — knowing them upfront will save you time.

Minimized windows may not appear in search results

getWindowsWithTitle() and getAllWindows() may not return minimized windows on some platforms. If you need to find a window that might be minimized, restore it first or use getAllWindows() and filter by title manually.

getWindowsWithTitle() is a substring search by default

Pass condition=pwc.Re.EQUALS for an exact match, or condition=pwc.Re.CONTAINS with flags=pwc.Re.IGNORECASE for case-insensitive partial matching. Window titles are language-dependent on some platforms (notably menu labels on Windows).

macOS is slow for Apple Script calls

Calls like getActiveWindowTitle() can take 400–500 ms on Apple Silicon Macs. This is an Apple Script limitation. For high-frequency polling, use the watchdog with a tuned interval rather than calling these methods in a tight loop.

Wayland support is limited

On Wayland (Ubuntu 22.04+, Fedora, and others), most window enumeration functions will fail or return empty results for system applications. Use X11/XWayland mode if you need full functionality, or instantiate Window directly with a known XID.

WSL2 is not supported

PyWinCtl requires a native X server environment. WSL2 does not expose the X Window information the library relies on.

sendBehind() only works reliably on Mint/Cinnamon and Ubuntu 22.04+

On other Linux setups, this method may silently fail. Check the Linux notice section for the tested configurations.


Install

Via pip:

python -m pip install pywinctl

Via uv:

uv add pywinctl

From a wheel file (replace x.xx with the actual version):

python -m pip install PyWinCtl-x.xx-py3-none-any.whl

Add --force-reinstall if you need to ensure the correct dependency versions are installed.

Then import it in your project:

import pywinctl as pwc

Requirements: Python ≥ 3.9. Platform dependencies are installed automatically: pywin32 on Windows, python-xlib + ewmhlib on Linux, pyobjc on macOS.


Support

Found a bug? Have a question or a suggestion? Open an issue on the project page. The maintainer is very responsive and typically replies within days.

You can also browse existing discussions — many common questions and platform-specific workarounds are already documented there.


Using this code

To contribute or run the code locally, fork the repository or download and unzip it, then install dev dependencies:

uv sync

or

python -m venv .venv
python -m pip install -e . --group=dev

Test

To run the test suite on your system, navigate to the tests folder and run:

uv run test_pywinctl.py