Python fcntl: File Locks and Control

Published on: August 25, 2026
Reading time: 5 minutes
Side view of contemplating female assistant in casual style standing near shelves and choosing file with documents

The fcntl module exposes the Unix fcntl() and ioctl() system calls together with convenient file-locking helpers. It works with file descriptors and reaches features that high-level file APIs do not expose, including descriptor flags, advisory locks, pipe configuration, terminal control, and device-specific operations.

This power is dangerous when used casually. Commands, constants, and C structures vary by operating system and architecture. A buffer with the wrong type or size can corrupt memory or crash the interpreter. Prefer higher-level libraries where possible and isolate native calls in small, tested functions.

Availability

fcntl is available on Unix and not on WASI. Windows has different APIs. Portable software should disable the feature or provide a verified platform-specific implementation.

try:
    import fcntl
except ImportError:
    fcntl = None

Do not pretend an incomplete fallback offers the same locking semantics. Incorrect coordination can allow two processes to modify the same state simultaneously.

File descriptors

Functions accept an integer descriptor or an I/O object whose fileno() returns a real descriptor.

with open("data.txt", "a+", encoding="utf-8") as file:
    fd = file.fileno()
    print(fd)

Descriptor numbers can be reused after closing. Never keep using an old integer after its owning object has been closed.

Exclusive locking with flock()

flock() is the clearest API for locking an entire file.

import fcntl

with open("state.lock", "a+") as file:
    fcntl.flock(file, fcntl.LOCK_EX)
    try:
        update_state()
    finally:
        fcntl.flock(file, fcntl.LOCK_UN)

LOCK_EX requests an exclusive lock, while LOCK_SH allows cooperating readers. These locks are normally advisory: every participant must use the same convention.

Non-blocking acquisition

Add LOCK_NB to fail immediately instead of waiting.

import errno
import fcntl

try:
    fcntl.flock(file, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as exc:
    if exc.errno in {errno.EACCES, errno.EAGAIN}:
        print("Another instance is running")
    else:
        raise

Check both error codes for portability. Python errno explains system error handling.

PID files are not locks

A file containing a PID helps diagnostics but does not create mutual exclusion. PIDs are reused and stale files remain after crashes.

import fcntl
import os

lock_file = open("app.lock", "a+")
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
lock_file.seek(0)
lock_file.truncate()
lock_file.write(str(os.getpid()))
lock_file.flush()

Keep the descriptor open for the full protected lifetime. Closing it normally releases the lock.

Region locks with lockf()

lockf() wraps record-locking operations and can protect a byte range.

import fcntl
import os

with open("database.dat", "r+b") as file:
    fcntl.lockf(file, fcntl.LOCK_EX, 128, 0, os.SEEK_SET)
    try:
        file.seek(0)
        update_record(file)
    finally:
        fcntl.lockf(file, fcntl.LOCK_UN, 128, 0, os.SEEK_SET)

Semantics differ across operating systems and filesystems. Test NFS, container volumes, shared storage, and special filesystems in the real deployment environment.

flock() or lockf()?

Use flock() for simple whole-file exclusion. Use lockf() when byte-range coordination is required and all participants follow the same record-locking model. Do not assume the two mechanisms interoperate.

Read status flags

F_GETFL returns the current file-status flags.

import fcntl
import os

flags = fcntl.fcntl(fd, fcntl.F_GETFL)
print(bool(flags & os.O_NONBLOCK))

To change one bit, preserve all existing flags.

fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

Replacing the full mask with only O_NONBLOCK may accidentally remove unrelated behavior.

Close-on-exec

FD_CLOEXEC prevents a descriptor from leaking into a program started through exec.

flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)

Modern Python creates many descriptors as non-inheritable, but software receiving external descriptors must still define ownership and inheritance.

Descriptor duplication

F_DUPFD and platform-specific CLOEXEC variants create another descriptor referring to the same open file description. The copies can share offsets and status flags.

Closing one descriptor does not necessarily close the underlying resource until every duplicate is closed.

ioctl()

ioctl() sends device- or terminal-specific requests.

import array
import fcntl
import termios

buffer = array.array("h", [0])
fcntl.ioctl(0, termios.TIOCGPGRP, buffer, True)
print(buffer[0])

The request number and buffer layout must come from the C documentation for the exact platform. Copying a structure from another architecture can crash the process.

Mutable buffers

With bytearray, array.array, or another writable buffer and mutate_flag=True, the system call can update the object in place. Python may use an internal 1024-byte area for smaller buffers, but the logical structure size must still be correct.

The official documentation explicitly warns that mismatched sizes can produce segmentation faults or subtle corruption.

Use struct.pack() carefully

Some operations expect a native C structure.

import struct

payload = struct.pack("hhllhh", lock_type, whence, start, length, pid, 0)

Alignment, integer size, endianness, and field layout vary. A format copied from a blog is not automatically portable. Prefer flock(), a maintained wrapper, or a compiled extension using the correct headers.

The 1024-byte limit

When fcntl() receives a bytes-like argument, the result has the same size and is limited to 1024 bytes. An operation returning a larger structure cannot be made safe by simply guessing another argument length.

Interrupted calls

In Python 3.14, ioctl() releases the GIL during the system call and retries failures caused by EINTR. Device errors, invalid commands, and malformed buffers still raise OSError.

Pipe capacity

Linux may provide F_GETPIPE_SZ and F_SETPIPE_SZ.

if hasattr(fcntl, "F_GETPIPE_SZ"):
    capacity = fcntl.fcntl(pipe_fd, fcntl.F_GETPIPE_SZ)
    print(capacity)

Increasing capacity can require privileges and consumes kernel memory. It is not a substitute for backpressure. See Python select.

memfd seals

Linux may expose F_ADD_SEALS, F_GET_SEALS, and F_SEAL_* for descriptors created with os.memfd_create(). Seals can prevent writing, shrinking, growing, or future writable mappings.

They are non-portable and some changes are irreversible. Apply them only after the data is complete.

Recent Linux systems may provide FICLONE and FICLONERANGE for copy-on-write clones on compatible filesystems. The call may fail across devices, on network filesystems, or on unsupported disk formats.

Implement a normal-copy fallback and validate the destination.

Open file description locks

Linux constants such as F_OFD_SETLK create locks associated with an open file description rather than a process. Their interaction with duplicated descriptors, fork, and threads differs from traditional locks.

Use them only when those semantics are specifically required. flock() remains clearer for ordinary single-instance protection.

Auditing

Calls raise auditing events including fcntl.fcntl, fcntl.ioctl, fcntl.flock, and fcntl.lockf. Restricted runtimes may observe or reject them.

Security

Do not accept a raw request number, command, descriptor, or packed structure from an untrusted user. That can expose devices and process resources to arbitrary control.

Use an allowlist, verify the descriptor type, and run with the least privileges possible.

Testing

Test competing processes, exception cleanup, crashes, local and remote filesystems, closed descriptors, subprocess inheritance, non-blocking flags, 32-bit and 64-bit builds, missing constants, and permission failures.

Lock semantics should be tested with separate processes rather than threads alone.

Common mistakes

Common failures include forgetting that locks are advisory, closing the file too soon, mixing flock and lockf, assuming NFS behaves like local disk, overwriting existing flags, reusing closed descriptors, copying a C structure from another platform, and passing a wrongly sized buffer.

Conclusion

fcntl provides low-level Unix control over files, pipes, locks, and devices. Prefer flock() for simple exclusion. For fcntl() and ioctl(), follow the exact platform headers and manuals.

Validate constants, preserve flags, minimize privileges, and test on the production operating system. Consult the official fcntl documentation, fcntl(2), and ioctl(2).

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python readline: History and Autocomplete

    Learn Python readline for command history, tab completion, line editing, GNU Readline, libedit, and safer interactive terminal prompts.

    Ler mais

    Tempo de leitura: 5 minutos
    25/08/2026
    Peaceful river scene with mossy boulders and flowing water captured in long exposure.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python io: Master Streams and Buffers

    Learn Python io for text and binary streams, buffering, encodings, StringIO, BytesIO, raw I/O, and file-like interfaces.

    Ler mais

    Tempo de leitura: 6 minutos
    24/08/2026
    Business professional analyzing financial data on multiple computer monitors at his workspace.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python select: Monitor Multiple I/O

    Learn Python select to monitor sockets and pipes, handle partial I/O, backpressure, poll, epoll, signals, and platform differences.

    Ler mais

    Tempo de leitura: 6 minutos
    24/08/2026
    Striking image of a red-bellied python showcasing its vibrant scales in dramatic lighting.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python signal: Graceful Process Shutdown

    Learn Python signal to handle SIGTERM and SIGINT, stop services, use timers and wakeup file descriptors, and avoid handler deadlocks.

    Ler mais

    Tempo de leitura: 6 minutos
    24/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python errno: Understand System Errors

    Learn Python errno to interpret system codes, handle OSError, files, networks, retries, and native calls portably.

    Ler mais

    Tempo de leitura: 4 minutos
    24/08/2026
    Side view of young cheerful ethnic female with book speaking on cellphone in public library while looking away
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ctypes: Call C Libraries

    Learn Python ctypes to load C libraries, define types and pointers, manage memory, callbacks, ABI, and native errors safely.

    Ler mais

    Tempo de leitura: 6 minutos
    24/08/2026