pypi / portalocker
portalocker
What changed in portalocker 3 to 4, from its official release notes, upgrade guides and advisories. Sources checked 2026-10-29.
30 breaking2 security5 deprecations20 new APIs and options1 performance9 behavior and defaults1 tooling46 fixes and other
See which of these apply to your code, ranked, in your agent:
npx @unstale/cli initportalocker 4113 changes
Breaking changes 29
Reacquiring a semaphore instance that already holds a slot now raises LockException instead of AssertionError.
Lock resolves its path with os.path.abspath at construction, so filename now contains an absolute path and later chdir calls do not redirect lock-file operations.
Flags are validated on every platform before system calls; UNBLOCK, SHARED | EXCLUSIVE, and flags naming no lock type raise RuntimeError.
On POSIX, modes containing w now clear O_APPEND after deferred truncation, so positioned writes behave like builtin open(mode='w'). Windows append semantics remain.
An exception from the with-block now takes precedence over a release error, which is chained as its context.
Only AlreadyLocked contention is retried. Other LockException failures now raise immediately, and custom lockers must raise AlreadyLocked for contention to be retried.
The finalizer that released locks and closed filehandles when a wrapper was collected has been removed, restoring the 3.2.0 semantics.
Deleting a lock stored as a class attribute now releases the lock itself instead of calling release on its owner.
Fallback LK_* values now match the system constants, and are stored on the locker instance rather than added to the shared msvcrt module.
Release failures no longer mask the with-body exception; they are chained to it, and release errors follow raise_on_release_error.
Plain LockException failures from the sidecar are no longer normalized to AlreadyLocked; terminal backend failures propagate as themselves.
PID parsing now accepts only plain positive ASCII decimal values; other inputs are reported as unreadable.
Context entry now raises AlreadyLocked when another process holds the lock but its PID is missing, unreadable, or invalid, instead of returning None.
An existing destination now raises FileExistsError on entry instead of AssertionError.
Legacy universal-newline U mode strings have been removed from the Mode literal, narrowing the static typing contract without changing runtime behavior.
The subscription uses a dedicated connection that never retries or reconnects; holders on flaky networks lose their locks rather than silently retaining an invalid hold.
A clean with-block exit raises LockLostError after releasing if the lock was lost; release itself does not raise due to loss.
Acquiring an instance that already holds a lock raises portalocker.LockException instead of AssertionError.
Forked children release only local references to inherited held locks rather than unsubscribing and releasing the parent's lock; a child needing a lock must create its own instance.
Worker failures are scoped to the affected lock: waiter subscription failures retry within the timeout, while a held lock's worker death marks it lost.
Without the optional Redis dependency, RedisLock is now a stub class whose constructor raises ImportError with the dependency and install command; callers should catch ImportError rather than compare it with None.
An inconsistent held-count state with no filehandle now raises LockException instead of relying on an assert that python -O removes.
Reacquiring a valid held lock is an idempotent no-op; if its path was externally unlinked or replaced, acquisition raises LockException without closing the held handle.
A failing POSIX module-level unlock raises portalocker.LockException instead of raw OSError; the original error remains available through args[0] and __cause__.
On POSIX, publication now raises FileExistsError and preserves a destination created while the context was open.
Windows shared locks require this optional extra; otherwise an informative ImportError is raised.
Exceptions now populate .strerror and pass the message as a second positional argument, matching the Windows contract; their str() output changes to a 2-tuple repr.
Python 3.10 or later is required; Python 3.9 support has been dropped.
No longer installed by default on Windows; exclusive locks use the dependency-free msvcrt-based locker, while shared locks require portalocker[win32].
Security 2
Promotion confirmation and election changes prevent competing writers from becoming simultaneous exclusive holders; mixed-version protection is complete once all channel writers run 4.2 or later.
The unlock-then-unlink race is fixed: POSIX release unlinks before unlocking, and acquire re-verifies file identity after locking.
Deprecations 5
Passing a bool as check_interval is still honored but now warns; it raises TypeError in 5.0.
Passing a bool as timeout is still honored but now warns; it raises TypeError in 5.0.
The default-name DeprecationWarning now points to the constructing caller and deduplicates per call site.
Deprecated because the package has never raised this exception; instantiating it now emits DeprecationWarning.
Deprecated because its reap arm can kill live-but-slow holders; use the read-only probe() to inspect channel participants.
New APIs and options 20
Adds the BinaryMode alias, used to drive the filehandle typing overloads.
Adds the TextMode alias, used to drive the filehandle typing overloads.
RedisLock provides a check for verifying ownership during long critical sections.
Exposes the token for a fenced exclusive grant; it is None without a fenced grant and survives loss and release until the next acquire.
Opt-in fencing increments a never-expiring channel fence key after exclusive-grant confirmation and provides a token for downstream resources to reject stale writes.
Controls whether loss interrupts the main thread; it defaults to True in 4.2, warns when unset, and is scheduled to default to False in 5.0.
Lock exceptions preserve the file name as a string in fh_name; pickling and deep-copying drop the file handle while shallow copies share it.
RedisLock exposes whether its lock has been lost.
A callback runs exactly once per loss on the keep-alive thread; it may call release() on the lock.
A new exception reports RedisLock loss and carries channel, holder_id, and the causal exception as __cause__.
A read-only method reports who is on the channel without side effects.
The exception raised by a failed self-check becomes the cause of LockLostError.
Opt-in self-checks publish a liveness ping at each interval and require a response within the configured interval or unavailable_timeout, surfacing failures as lock loss.
RedisLock accepts a factory for deriving its dedicated subscription connection, supporting exotic setups such as Sentinel, cluster, and custom pools.
Exposes the competing PID when it is readable.
Cleanup failures remain suppressed by default, but callers can opt into reporting them; closing is always attempted and the file handle reference is cleared.
NamedBoundedSemaphore is now exported from the top-level portalocker namespace.
Adds ownership-only contexts; contention raises AlreadyLocked before entering the body.
Adds pidfile-based locking.
Adds shared readers; waiting writers gate new readers to prevent starvation, holder-specific heartbeats preserve stale-client cleanup, and legacy responses are treated as exclusive for mixed-version safety.
Performance 1
Probe reply draining collects in-flight and buffered replies in the same pass, sharply reducing probe duration and staleness windows.
Behavior and default changes 9
Lock is generic over its filehandle type. Literal text and binary modes infer IO[str] and IO[bytes]; a bare Lock defaults to Lock[IO[str]], matching the default mode.
RLock is generic over its filehandle type. Literal text and binary modes infer IO[str] and IO[bytes]; a bare RLock defaults to the text filehandle type.
A second acquire on an instance that already holds the lock is now an idempotent no-op that does not touch the sidecar lock or PID file.
Publication falls back to an existence check and rename on filesystems without hard-link support; the fallback cannot reliably refuse concurrent publishers.
With fail_when_locked, acquisition raises only after a conclusive probe; inconclusive probes retry within timeout, and timeout=0 preserves strict single-attempt behavior.
Verification retries now share one deadline and receive only the remaining timeout budget.
The --output-file option now opens lazily.
With fail_when_locked=True, acquisition fails fast instead of polling for the full timeout.
No longer keeps a strong atexit reference to itself, allowing unused instances to be garbage collected while interpreter-exit cleanup continues via a weak reference.
Tooling 1
The build backend requirement is now uv_build>=0.11,<1, allowing other build frontends and distribution packagers to build the sdist.
Fixes and other changes 46
Anchors a relative destination when the context is entered, so changing the working directory inside the body no longer causes FileNotFoundError or leaves the temporary file behind after a failing body. The yielded handle name and FileExistsError paths are now…
Cancellation during setup or between attempts releases resources, preserves the original exception, and permits reuse.
Polling retains a final read so buffered holder responses are processed before deciding that a holder is unavailable.
Retry sleeps are capped at the acquisition deadline, so a large check_interval no longer adds a full interval after timeout.
Lock now accepts subscription at runtime, allowing expressions such as Lock[typing.IO[bytes]] that raised TypeError in 4.2.0.
RLock.__init__ now initializes through Lock._init instead of Lock.__init__; monkeypatched constructors will notice.
RLock now accepts subscription at runtime.
Its filehandle type is pinned to IO[str].
Lock.acquire with fail_when_locked now propagates the locker's arguments so strerror is populated, while the original error remains in args[0] and __cause__.
Concurrent acquisition on one instance no longer leaks a slot: a losing thread returns its extra slot and receives LockException.
The blocking-mode timeout warning now fires at most once per lock instance and points to the caller, including through subclass constructors.
Concurrent acquire calls on one Lock instance are documented as unsupported; use one instance per thread because POSIX record locks are process-wide.
Failed acquisition paths, including interrupts, now unlock and close opened descriptors before returning or propagating; PidFileLock rolls back its sidecar too.
If file preparation fails after the lock is taken, the handle is now unlocked and closed before the original error escapes.
Concurrent or reentrant releases atomically claim lock state so only one caller tears the lock down.
Sleep intervals are capped by the remaining deadline, preventing acquisition from exceeding its timeout by up to a full check_interval.
Exceptions carrying open file handles can now cross multiprocessing boundaries without failing to pickle; detached handles whose name lookup fails no longer break exception construction.
Interpreter-exit cleanup now releases only locks constructed by the exiting process, avoiding removal of lock files inherited by a forked child.
PID publication now writes a neighboring temporary file and replaces the destination, so readers see either the old complete PID or the new complete PID.
Per-call timeout and check_interval arguments are now coalesced with instance attributes before constructing the sidecar Lock.
The sidecar reference is published only after successful acquisition; interrupted attempts roll back and release refuses to unlink files when the sidecar has no held handle.
PID files are read as bytes and validated as ASCII digits; undecodable content and digit runs longer than 4300 digits return None.
On Windows, the PID file is now unlinked before releasing the sidecar lock, preventing cleanup from deleting a successor's newly published PID.
The temporary file is created with mode 0o666 so the process umask determines destination permissions as with plain open.
Failed publication now keeps the temporary file and names its path in the exception; exceptions from the caller's body remove the temporary file, and closing the handle in the body no longer breaks publication.
The command now reads and assembles inputs before opening --output-file, preventing malformed input from truncating an existing output.
TemporaryFileLock and PidFileLock constructors now accept and forward this documented keyword; PidFileLock follows it when handling unlink errors.
Acquisition failures now clean up subscriptions and workers before propagating non-retryable errors, leaving the instance reusable.
RedisLock acquisition now waits for subscription confirmation, serializes promotions and ping answers, and checks subscriber counts again before pinging, closing races that could let two writers hold the lock.
Acquire counts and their transitions are protected by the instance state lock, preventing lost increments and handle closure while a hold remains outstanding.
A single module-level exit hook now releases held locks, replacing one never-unregistered callback per constructed instance.
Release errors now honor raise_on_release_error=False on teardown and are logged at warning level instead of disappearing.
TemporaryFileLock and PidFileLock now annotate filename as types.Filename, allowing pathlib.Path arguments to pass static type checking.
Each LockFileEx and UnlockFileEx call now receives a fresh OVERLAPPED structure instead of reusing a cached instance across calls and threads.
OSError from the Windows lock path is now wrapped in LockException, matching the unlock path.
No longer stays permanently Already locked after a non-contention error, such as a missing directory.
On POSIX, now uses its named syscall instead of silently using the global LOCKER.
The module-level POSIX lock() and unlock() now accept all documented LOCKER forms: tuple, instance, or class.
LockBase is now generic over the acquire return type; downstream Lock subclasses are unaffected.
Unexpected Win32 errors now raise LockException, matching the documented contract.
Windows locking now starts at byte 0 for raw file descriptors, preserving mutual exclusion on files larger than 64KiB; unexpected Win32 errors raise LockException.
The timeout is no longer ignored when fail_when_locked=False.
PID-publication failures now transactionally release the sidecar and preserve the publication error if cleanup also fails.
Vendored single-file output now correctly includes RedisLock instead of always producing None.
Fixes crashed-holder detection and cleanup: the liveness ping follows active subscription confirmation, pubsub connections do not leak on reap, internally created connections close on release, and failed acquisition rolls back for retry.
Release is a no-op when the object does not hold the lock, preventing a stale object from unlinking the current holder's lock file.
portalocker 31 changes
Breaking changes 1
Support for deprecated Python versions was dropped; only Python 3.9 and up are supported.