Troubleshooting

Six symptoms that show up more than once, each with its cause and its fix. This page assumes you already picked a lock class and are past the happy path; for that groundwork see Quickstart and Lock Types.

My lock doesn’t stop another process from writing

Cause: portalocker locks are advisory on POSIX: a process that never asks for the lock is never blocked by it, and never learns one exists. See Platform Behaviour for the full explanation, including a demonstration of the clobber and why the same code is enforced on Windows.

Fix: there is no flag that makes POSIX locking mandatory. Every process that touches the file, including code you do not control, has to take the same lock before reading or writing it.

ImportError when taking a shared lock on Windows

Cause: requesting a shared lock (flags=portalocker.LockFlags.SHARED) on Windows needs the optional pywin32 package, no longer installed by default since 4.0.0 – see Platform Behaviour for why msvcrt alone cannot provide one. Without it, the failure reads:

ImportError: Shared locks on Windows require the win32 extra
(pywin32); msvcrt provides no true shared lock. Install it with:
pip install "portalocker[win32]"

Through the module-level portalocker.lock function, or a directly constructed Win32Locker, that ImportError propagates unchanged. Through Lock.acquire, it is caught like any other exception and re-raised as a LockException wrapping it, so the message above still appears in str(exc) even though the exception type is different.

Fix: install the extra.

pip install "portalocker[win32]"

The other process doesn’t see my data

Cause: a lock serialises access, not visibility – your bytes can still be sitting in a buffer when another process looks. See “Flush before you release” in Platform Behaviour for the two layers involved.

Fix: call flush(), then os.fsync, before another process needs to see the write:

>>> import os
>>> import portalocker
>>> with portalocker.Lock('output.txt', 'w', timeout=5) as fh:
...     _ = fh.write('visible to the next reader')
...     fh.flush()
...     os.fsync(fh.fileno())

AlreadyLocked raised immediately without waiting

Cause: two independent settings skip the retry loop and fail on the very first contended attempt instead of waiting up to timeout:

  • fail_when_locked=True. TemporaryFileLock and PidFileLock both default to this, unlike Lock, so switching to either one changes this behaviour even if timeout itself is left untouched – see Lock Types.

  • timeout=0. The retry clock in LockBase._timeout_generator only starts after the first attempt, so a timeout of 0 still buys exactly one attempt and nothing more, regardless of fail_when_locked.

>>> import portalocker
>>> first = portalocker.Lock('contended.lock', fail_when_locked=True)
>>> _ = first.acquire()
>>> second = portalocker.Lock('contended.lock', fail_when_locked=True)
>>> try:
...     second.acquire()
... except portalocker.AlreadyLocked:
...     print('raised on the first attempt, no retry')
raised on the first attempt, no retry
>>> first.release()

Fix: if retrying for the full timeout is what you want, leave fail_when_locked at its default (False) and pass a positive timeout.

A stale lock file is left behind after a crash

Cause: TemporaryFileLock and PidFileLock remove their lock file in release, with two fallbacks for code that forgets to call it: garbage collection (LockBase.__del__) and an atexit handler registered when the lock is constructed. None of those three run when the process is killed outright – SIGKILL, a segfault, a lost VM – so the file stays on disk.

This is expected, and mostly harmless. The kernel releases the process’s advisory lock the moment it dies, file present or not, so the next acquire on the same path succeeds immediately; the leftover file is inert litter, not a lock that needs clearing by hand. For PidFileLock specifically, PidFileLock.read_pid can then report a PID that no longer exists – it only ever says who last wrote the file, never whether that process is still alive.

4.0.0 fixed a related but distinct race in the same area, tracked as issue #115: a competing acquirer could open and lock the very inode a releaser was in the middle of unlinking, so both processes believed they held the lock at once (split-brain). acquire now re-verifies, after locking, that its handle still names the current path, and retries within the timeout if a race is detected.

Fix: nothing to do for the stale file itself; it is safe to ignore or delete. See Lock Types for what TemporaryFileLock guarantees on release, and PidFileLock.release for the PID file’s own release behaviour.

str() of a lock exception changed after upgrading

Cause: before 4.0.0, str(exc) on POSIX was whatever the bare OSError from fcntl reported. Since 4.0.0, every lock failure – POSIX and Windows alike – passes an error code as the first argument and the message as the second, matching the two-argument convention BaseLockException already followed on Windows. That makes str(exc) on POSIX a 2-tuple repr instead of the bare message:

>>> from portalocker import exceptions
>>> exc = exceptions.LockException(
...     exceptions.LockException.LOCK_FAILED,
...     'Resource temporarily unavailable',
... )
>>> str(exc)
"(1, 'Resource temporarily unavailable')"
>>> exc.strerror
'Resource temporarily unavailable'

Fix: read BaseLockException.strerror instead of parsing str(exc); it has held the message consistently on both platforms since 4.0.0. See Migrating from 3.x to 4.0.0 for the rest of what changed in that release.