skip to content

What is the difference between fcntl.flock and fcntl.lockf in Python?

level: seniorimportance: nice to knowfreq 14%

answer

  1. Two calls, two ownership models
  2. One belongs to the open description
  3. The other belongs to the process id
  4. Any close can drop the record lock
  5. Byte ranges versus the whole file

basics

~20 s

fcntl.flock takes a whole-file lock owned by the open file description, so duplicated and inherited descriptors share it. fcntl.lockf takes a POSIX byte-range lock owned by the process, dropped when it closes any descriptor to that file.

solid answer

~50 s

`fcntl.flock` wraps the BSD-style whole-file lock: ownership belongs to the open file description, so descriptors duplicated with `os.dup` or inherited across `os.fork` share one lock, and it is released only when the last of them closes. `fcntl.lockf` wraps POSIX record locks, which are byte-range — the signature takes a length, a start and a whence, with a length of zero meaning to end of file — and owned by the **process**: they are not inherited by a forked child, and, the classic footgun, they are dropped the moment the process closes *any* descriptor referring to that file, even one opened later by unrelated code. The two families are separate on Linux, so a program using one is not excluded by a program using the other. Error reporting differs too: a non-blocking `fcntl.flock` conflict raises `BlockingIOError`, while `fcntl.lockf` may raise that or `PermissionError`, so catch `OSError`. For a single-instance guard, prefer `fcntl.flock`.

code

python · 18 lines
python
import fcntl, os, sys

path = "/tmp/lockf-demo.lock"
fd1 = os.open(path, os.O_CREAT | os.O_RDWR, 0o644)
fcntl.lockf(fd1, fcntl.LOCK_EX | fcntl.LOCK_NB)

fd2 = os.open(path, os.O_RDWR)
os.close(fd2)

if os.fork() == 0:
    fd = os.open(path, os.O_RDWR)
    try:
        fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        print("child took the lock: closing the second descriptor dropped it")
    except OSError:
        print("child blocked: the lock is still held")
    sys.exit(0)
os.wait()

go deeper

for a junior

It is enough to know both live in the fcntl module, both are advisory, and they are not the same lock — so two programs guarding one file must use the same call as each other.

for a middle

Say which is whole-file and which is byte-range, and name the ownership difference: open file description versus process. Mention that a non-blocking conflict is reported by exception, not by a return value.

for a senior

Show the operational consequences: the close-any-descriptor rule that silently drops a record lock, the opposite fork behaviour of the two families, and the interoperability trap of mixing them across cooperating programs.

for a principal

Set a house default so services do not mix families by accident, and know when the byte-range family is genuinely required — coordination inside one file, or interoperation with software whose locking you do not control.

### Two different locking families behind one module The `fcntl` module exposes two locking calls that look interchangeable — both take a descriptor and both accept `fcntl.LOCK_EX`, `fcntl.LOCK_SH`, `fcntl.LOCK_UN` and `fcntl.LOCK_NB` — and are not. They wrap different underlying system calls with different ownership models. **`fcntl.flock(fd, operation)`** is the BSD whole-file lock. The lock belongs to the *open file description*: the object created by one successful open, which `os.dup` and `os.fork` copy references to rather than duplicating. Every descriptor sharing that description shares the one lock; taking it twice through the same description is a no-op upgrade rather than a conflict. It is released by `fcntl.LOCK_UN`, by closing the last descriptor that refers to that description, or by the process ending. **`fcntl.lockf(fd, cmd, len=0, start=0, whence=0)`** is the POSIX record lock, implemented with the locking commands of `fcntl(2)`. It locks a byte range — by default from the current position, and a length of `0` means "to the end of the file, however it grows" — and the lock belongs to the *process*, identified by pid. ### The three consequences that matter **Closing any descriptor drops a record lock.** POSIX says all of a process's record locks on a file are released when that process closes *any* descriptor for that file. Your job locks the file at startup; a hundred lines later some helper opens the same path to read it and closes it again; the guard silently evaporates while your code still holds its original descriptor and believes it is protected. `fcntl.flock` has no such rule — only closing descriptors on the locking description matters. **Fork behaves oppositely.** A forked child inherits `flock` locks (it inherits the descriptions), and does *not* inherit record locks, which stay with the parent's pid. So for a pre-fork worker pool, `flock` taken before the fork leaves every child sharing the lock — often surprising — while `lockf` leaves only the parent holding it. **Same process, second descriptor.** Two separate opens of the same file in one process create two descriptions, so two `fcntl.flock` calls on them genuinely conflict, and a careless program can block against itself. Record locks are per-process, so a second `fcntl.lockf` in the same process just adjusts the existing lock instead of conflicting — which is convenient, and also means a same-process test can never demonstrate that a record lock is held. ### What each is actually for `fcntl.lockf` earns its keep when you need *ranges*: several processes coordinating over disjoint regions of one file, the mechanism behind classic record-oriented storage. It is also the family that a great deal of other software uses, so if you must interoperate with a program that takes POSIX record locks, you must take them too — on Linux the two families are independent, and a `flock` lock does not exclude a `lockf` lock on the same file at all. Mixing them across cooperating programs is a real and silent bug. `fcntl.flock` is the better default for a single-instance guard: whole-file semantics are what you mean, the ownership model is the one people expect, and the close-any-descriptor rule cannot ambush you. ### Error handling With `fcntl.LOCK_NB`, a conflict surfaces as an exception rather than a wait. `fcntl.flock` raises `BlockingIOError`. `fcntl.lockf` may raise `BlockingIOError` or `PermissionError`, because the underlying call is permitted to report either of two error numbers for "already locked", and Python maps them to different exception classes. The portable habit is to catch `OSError` and, if you need to distinguish, inspect the `errno` attribute rather than the class. ### Practical shape ```python import fcntl, os, sys fd = os.open("/tmp/job.lock", os.O_CREAT | os.O_RDWR, 0o644) try: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) # whole file, tied to this description except OSError: sys.exit(0) ``` Both calls are advisory: they constrain only processes that also lock. Both are Unix-only, since the `fcntl` module does not exist on Windows. And both leave the file itself untouched — the difference between them is entirely about *who owns the lock and when it ends*, which is the answer an interviewer is listening for.

  • Why can a program using fcntl.flock fail to exclude a program using fcntl.lockf on the same file?
    On Linux they are independent lock families maintained separately by the kernel, so a whole-file BSD lock and a POSIX record lock on one file do not see each other. Mutual exclusion only exists between processes using the same family, which is why an interoperating tool must match whatever the other software takes rather than picking by preference.
  • Which of the two would you pick for coordinating writes to disjoint regions of one data file?
    fcntl.lockf, because it is the byte-range family: you pass a length and a start, so two processes can hold non-overlapping locks on the same file simultaneously. fcntl.flock only ever locks the whole file, so it would serialise writers that never actually conflict.

saying these in an interview costs you the question

  • Treats the two calls as interchangeable wrappers
  • Unaware that closing any descriptor drops a record lock
  • Assumes a forked child inherits both kinds equally
  • Expects one family to exclude the other on Linux
  • Catches only BlockingIOError from a non-blocking lockf

context