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

    Python asynchronous code on a laptop for inspect.markcoroutinefunction
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    markcoroutinefunction: Detect Async Wrappers

    Learn inspect.markcoroutinefunction in Python to identify async wrappers, integrate frameworks, and avoid incorrect coroutine detection.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python code for traversing folders and files with Path.walk
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Path.walk: Traverse Directories Safely

    Learn Python Path.walk to traverse directories, filter files, handle errors, and control directory-tree processing safely.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Python process debugging in a terminal with source code
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: Debug Running Python Processes

    Learn how to attach pdb to a running Python process, inspect stacks, and diagnose hangs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026
    Python programming code for exact fractions
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    fractions.from_number: Convert Numbers to Fractions

    Learn Python fractions.from_number for exact rational conversion, float and Decimal handling, approximation, validation, and safe arithmetic.

    Ler mais

    Tempo de leitura: 4 minutos
    09/10/2026
    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026