PyAutoGUI Coordinates on DPI-Scaled Multi-Monitor Setups: The Complete Fix

What PyAutoGUI would see on your screen:

Physical Resolution
What PyAutoGUI Sees (without fix)
DPR
Click Offset (worst case)

You installed PyAutoGUI, wrote pyautogui.click(960, 540), and the click landed somewhere completely different. Or pyautogui.locateOnScreen() returns the wrong coordinates. Or your screenshot captures the wrong region. Sound familiar?

These are the three classic PyAutoGUI coordinate bugs, and they share one root cause: DPI scaling + multi-monitor virtual screen coordinates. We tackle all three below — with code you can paste straight into your project. Your screen's numbers are detected above.

Open Free Screen Coordinates Tool

The Three PyAutoGUI Coordinate Bugs

PyAutoGUI coordinate problems almost always fall into one of these three categories:

  1. Click offset on high-DPI displays: pyautogui.click(960, 540) does not land where you expect because PyAutoGUI is using logical coordinates while the target application uses physical coordinates. Covered in our DPI Scaling guide.
  2. Wrong screen size reported: pyautogui.size() returns 2560×1440 on your 4K display because Windows is feeding it the logical (scaled) resolution. Your scripts calculate wrong center points, wrong percentages, wrong everything.
  3. Screenshot region misalignment: pyautogui.screenshot(region=(x, y, w, h)) captures a different area than the one you specified, because the region coordinates are in logical space but the screenshot uses the physical pixel buffer.

All three bugs have the same root cause: PyAutoGUI runs as a non-DPI-aware process on Windows by default. The OS feeds it logical coordinates. The fix is always the same — make it DPI-aware.

pyautogui.size() vs Your Real Resolution

What does pyautogui.size() actually return on a 4K display? Depends on your scaling:

Physical ResolutionWindows Scalepyautogui.size() ReturnsMissing Pixels
3840×2160100%3840×2160 ✓None
3840×2160125%3072×1728768×432 pixels off
3840×2160150%2560×14401280×720 pixels off
3840×2160200%1920×10801920×1080 pixels off
2560×1440125%2048×1152512×288 pixels off
2560×1440150%1707×960853×480 pixels off

At 150% scaling on 4K, PyAutoGUI thinks your screen is only 2560 pixels wide. When you click what it thinks is the center (1280, 720), Windows translates that to physical pixel (1920, 1080) — which happens to be the real center. But any other position will be off by the scale factor.

The Complete Fix: DPI-Aware PyAutoGUI Setup

Put this at the top of your script — before importing PyAutoGUI:

import ctypes
import sys

# Fix #1: Tell Windows we want real physical pixels
if sys.platform == 'win32':
    try:
        # Per-monitor DPI awareness (best for multi-monitor)
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except Exception:
        # Fallback: system-level DPI awareness
        ctypes.windll.user32.SetProcessDPIAware()

# NOW import PyAutoGUI — it will see real pixels
import pyautogui

# Verify the fix worked
w, h = pyautogui.size()
print(f"Screen size: {w}×{h}")
# On a 4K display at 150%, this should now print 3840×2160

Why SetProcessDpiAwareness(2) Instead of SetProcessDPIAware()?

CallLevelMulti-MonitorUse When
SetProcessDPIAware()SystemUses primary monitor's DPI for all monitorsSingle monitor only
SetProcessDpiAwareness(2)Per-monitorEach monitor reports its true physical resolutionAny setup (recommended)

If you have a 4K laptop screen at 150% and an external 1080p monitor at 100%, SetProcessDpiAwareness(2) ensures PyAutoGUI sees 3840×2160 on the laptop screen and 1920×1080 on the external — the real numbers for each display. For more on how coordinates work across different monitors, see our Multi-Monitor Coordinates guide.

Fixing Screenshot Region Offsets

The screenshot region problem is sneaky. Without the DPI fix, the region parameter uses logical coordinates — so this happens (example assumes 150% scaling on a 4K display; your detected DPR may differ):

# WITHOUT the fix (150% scaling on 4K):
# You want to capture the top-left 960×540 pixels
img = pyautogui.screenshot(region=(0, 0, 960, 540))
# But it actually captures the logical top-left 960×540
# which maps to physical (0, 0, 1440, 810) — way more than you wanted

# WITH SetProcessDpiAwareness(2):
# Now the region uses physical pixels correctly
img = pyautogui.screenshot(region=(0, 0, 960, 540))
# Captures exactly the top-left 960×540 physical pixels ✓

Important: After enabling DPI awareness, pyautogui.screenshot() returns an image at the full physical resolution. On a 4K display, that is a 3840×2160 image, not 2560×1440. If you were cropping screenshots before the fix, you need to update your crop coordinates to physical pixels.

Multi-Monitor: Virtual Screen Coordinates Explained

On a multi-monitor setup, PyAutoGUI works with a single virtual screen that spans all your monitors. The primary monitor starts at (0, 0). Others extend from there:

# Check your virtual screen size
import ctypes
import pyautogui

# After DPI fix:
print(pyautogui.size())   # Primary monitor size
x, y = pyautogui.position()
print(f"Current mouse position: ({x}, {y})")
# Negative values mean you're on a monitor to the left/above

For the complete coordinate math for calculating any monitor's center, offsets, and edge positions, see our Multi-Monitor Coordinates guide.

Why locateOnScreen() Returns Wrong Coordinates

pyautogui.locateOnScreen('button.png') is one of PyAutoGUI's most useful features — and one of the most broken on high-DPI displays. The issue (example assumes 150% scaling on a 4K display):

  1. You take a screenshot of a button on your 4K display (the image is 3840×2160 physical pixels)
  2. You crop out the button and save it as button.png
  3. locateOnScreen() takes a screenshot — but without DPI awareness, Windows returns a logically-sized image (2560×1440) that has been resampled and blurred by the OS (DPI virtualization). The pixels no longer match your reference image exactly.
  4. It tries to find your crisp 4K-cropped button inside this blurry, downscaled screenshot — and fails or returns wrong coordinates

The fix: After calling SetProcessDpiAwareness(2), PyAutoGUI's internal screenshots capture raw physical pixels without OS resampling. Your reference image and the search screenshot are now at the same resolution and sharpness, so locateOnScreen() works correctly.

Tip: Always create your reference images after enabling DPI awareness. If you captured them before the fix, they are at logical resolution and will not match the physical-pixel screenshots.

Drop-In Script

Copy-paste this into your project. It handles DPI awareness, multi-monitor detection, and gives you helper functions for resolution-independent clicks:

"""
PyAutoGUI DPI-aware setup — drop this at the top of any script.
Works on Windows, macOS, and Linux. Multi-monitor safe.

CRITICAL: DPI awareness MUST be set before importing pyautogui,
otherwise it caches the logical resolution on import.
"""
import ctypes
import sys

# --- Set DPI awareness BEFORE importing pyautogui ---
if sys.platform == 'win32':
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except Exception:
        try:
            ctypes.windll.user32.SetProcessDPIAware()
        except Exception:
            pass

# NOW it is safe to import pyautogui
import pyautogui

def get_screen_info():
    """Return useful screen info."""
    w, h = pyautogui.size()
    return {
        'width': w,
        'height': h,
        'center': (w // 2, h // 2),
        'total_pixels': w * h,
    }

def click_relative(x_pct, y_pct):
    """Click at a percentage of the screen size (0.0 to 1.0).
    Resolution-independent — works on any screen."""
    w, h = pyautogui.size()
    pyautogui.click(int(w * x_pct), int(h * y_pct))

# --- Usage ---
info = get_screen_info()
print(f"Primary screen: {info['width']}×{info['height']}")
print(f"Center: {info['center']}")

# Example: click the center of the screen
click_relative(0.5, 0.5)

Frequently Asked Questions

Does this fix work on macOS and Linux?

SetProcessDpiAwareness() only exists on Windows. On macOS, PyAutoGUI handles Retina scaling well enough on its own. On Linux it depends on your display server (X11 vs Wayland). The setup_dpi_awareness() function above is safe to call anywhere — it just does nothing on non-Windows systems.

Why does my script work in IDLE but not when run directly?

Some IDEs — including IDLE — are already DPI-aware. Run your script inside one and it inherits that context. Run the same script from the command line or Task Scheduler and it starts as a non-DPI-aware process. Always call SetProcessDpiAwareness(2) regardless of how you launch.

Can I use this with PyAutoGUI + OpenCV together?

Yes. After enabling DPI awareness, pyautogui.screenshot() returns a full-resolution PIL image. You can convert it to a NumPy array for OpenCV processing. The coordinates from cv2.matchTemplate() will then match PyAutoGUI's physical-pixel coordinate space.

What if SetProcessDpiAwareness(2) throws an error?

It can fail if your process has already been marked as DPI-unaware (e.g., Python was launched by a non-DPI-aware parent process). The fallback is SetProcessDPIAware(). If even that fails, you are likely running in a context where DPI awareness is inherited from the parent — check your Python launcher or terminal settings.

How do I get my mouse coordinates to verify the fix?

Run print(pyautogui.position()) after calling setup_dpi_awareness(). Move your mouse to the bottom-right corner of your screen — it should read close to (width-1, height-1) where width/height are the physical resolution. Or use our online screen coordinates tool to see your position in real time.

Open Live Screen Coordinate Tracker DPI Scaling & Coordinates Guide