PyWinCtl¶
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
subprocessGet 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, orOpenCVControl 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:
Table of contents¶
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 |
|---|---|---|
(GET) title |
||
(GET) updatedTitle (MacOSWindow only) |
||
(GET) isMaximized |
||
(GET) isMinimized |
||
(GET) isActive |
||
(GET) isVisible |
||
(GET) isAlive |
||
Position / Size (via PyWinBox) |
||
(GET/SET) position (x, y) |
||
(GET/SET) left, top, right, bottom |
||
(GET/SET) topleft, topright, bottomleft, bottomright |
||
(GET/SET) midtop, midleft, midbottom, midright |
||
checkPermissions (macOS only) |
(GET/SET) center, centerx, centery |
|
(GET/SET) size (width, height) |
||
(GET/SET) width, height |
||
(GET/SET) box (x, y, width, height) |
||
(GET/SET) rect (x, y, right, bottom) |
||
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 watching with the given callbacks |
|
Replace active callbacks (pass all desired ones) |
|
Change the polling interval |
|
setTryToFind (macOS only) |
Try to locate window after title change |
Check if the watchdog is still running |
|
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 asNone) 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