2025, Nov 06 01:00

Preventing parallel device access on Windows with Python: correct Global mutex naming and reliable ctypes error handling

Learn how to enforce single-device ownership in Python on Windows using a named mutex: use the Global namespace, proper CreateMutexW signatures and GetLastError

When a Python application needs to coordinate access to a single hardware device across multiple processes, a named mutex is a natural choice. The idea is simple: each running instance tries to acquire a mutex keyed to the device identifier; if the mutex already exists, the process knows the device is in use and bails out. But on Windows, a named mutex is visible to other processes only if you name it correctly and read errors correctly. Otherwise, you might end up with multiple instances running in parallel, each convinced it owns the device.

Reproducing the issue

The following minimal example tries to guard a device with a named mutex, but it doesn’t stop concurrent instances. Both processes happily run and report a confusing error code instead of detecting contention.

import ctypes as c
import sys

ERR_ALREADY_EXISTS = 183

sync_handle = c.windll.kernel32.CreateMutexW(None, True, "my-app\\1234")
last_err = c.GetLastError()
print(f"Error code received: {last_err}")

if last_err == ERR_ALREADY_EXISTS:
    print("Another instance is already using the device.")
    sys.exit(0)

input("Program running. Press Enter to exit...")

What’s actually wrong

The mutex name is the first culprit. Windows uses specific namespaces for named kernel objects. You can’t invent arbitrary prefixes like my-app\.... To make a mutex visible across processes, use one of the supported namespaces. In this scenario the correct choice is Global\, while Local\ is also valid depending on needs. Without a supported namespace, separate processes won’t see the same object, which defeats synchronization.

The second problem is how the error is read. With ctypes, you should load the DLL with use_last_error=True and explicitly declare function prototypes. That ensures GetLastError() is captured immediately after the call and that bad results are converted into exceptions, instead of silently returning ambiguous values.

The fix

The corrected approach uses the Global\ namespace so all processes refer to the same mutex name, and it wires up the Win32 calls with proper types, error handling, and reliable access to GetLastError().

import ctypes as c
import ctypes.wintypes as wt

ERR_EXISTS = 183

# Convert invalid results into exceptions for predictable control flow.
def _raise_on_null(res, func, args):
    if not res:
        raise c.WinError(c.get_last_error())
    return res

def _raise_on_zero(res, func, args):
    if res == 0:
        raise c.WinError(c.get_last_error())

# Capture GetLastError() immediately after each ctypes call.
k32 = c.WinDLL("kernel32", use_last_error=True)

# Declare signatures for the Win32 APIs we call.
_CreateMutexW = k32.CreateMutexW
_CreateMutexW.argtypes = (c.c_void_p, wt.BOOL, wt.LPCWSTR)
_CreateMutexW.restype = wt.HANDLE
_CreateMutexW.errcheck = _raise_on_null

_CloseHandle = k32.CloseHandle
_CloseHandle.argtypes = (wt.HANDLE,)
_CloseHandle.restype = wt.BOOL
_CloseHandle.errcheck = _raise_on_zero

try:
    hm = _CreateMutexW(None, True, "Global\\1234")
except OSError as exc:
    print(exc)
else:
    try:
        if c.get_last_error() == ERR_EXISTS:
            print("Another instance is already using the device.")
        else:
            input("Program running. Press Enter to exit...")
    finally:
        _CloseHandle(hm)

Why this matters

Device access conflicts are notoriously hard to debug because they often reproduce only under load or on end-user machines. Using the correct namespace ensures that every process competes on the same synchronization primitive. Proper type declarations and error handling around Win32 calls prevent misleading diagnostics and make behavior deterministic, which cuts down on support overhead and field failures.

Takeaways

Use a supported namespace for named kernel objects; for cross-process visibility in this context, Global\ is the right pick. Bind Win32 APIs in ctypes with explicit argtypes and restype, enable use_last_error=True, and convert failure results into exceptions. With these two pieces in place, your mutex-based single-owner rule for hardware devices will work as intended and your application will behave predictably when multiple instances are launched.

The article is based on a question from StackOverflow by Ilya and an answer by Mark Tolonen.