How do Python's immortal objects and free-threaded build affect copy-on-write after fork?
answer
- Some objects never change reference count
- A saturated count that skips writes
- PEP 683 landed in Python 3.12
- Threads share one heap instead
basics
~20 sImmortal objects, added in Python 3.12, carry a saturated reference count that increment and decrement skip, so their pages really do stay shared after a fork. The free-threaded build reduces reference-count traffic but still writes per-object headers.
solid answer
~50 sPEP 683 (Python 3.12) gave a fixed set of interpreter objects — `None`, `True`, `False`, the small integers, statically allocated types and singletons — a saturated reference count that increment and decrement detect and skip. Their headers are therefore never written, and improving copy-on-write behaviour for pre-fork servers was one of the stated motivations. The catch is scope: it covers interpreter-owned objects, not the table your startup code parsed, so it removes a slice of the erosion rather than solving it. The free-threaded build, experimental in 3.13 and officially supported in 3.14 under PEP 779 with a 5–10% single-threaded penalty, keeps per-object counting but makes the owning thread's updates non-atomic and defers counting on some long-lived objects; pages still get dirty. Its real effect is strategic: threads in one heap share the data once, permanently, so the pre-fork trick stops being necessary.
code
python · 9 linesimport sys
# Immortal objects report a saturated reference count
print(sys.getrefcount(None) > 2**30)
print(sys.getrefcount(27) > 2**30)
# An ordinary object does not
flight = ["LH441", 27]
print(sys.getrefcount(flight))go deeper
Recall that a few objects — None, True, False and the small integers — are special in CPython, and that Python 3.12 changed how their reference counts behave rather than how you use them.
Explain immortality as a saturated count that increment and decrement skip, so those objects' headers are never written and the pages holding them are never copied after a fork.
Be precise about the limits: immortality covers interpreter-owned objects rather than your data, and the free-threaded build reduces reference-count traffic without removing per-object header writes.
Weigh the 3.14 options against each other: a pre-fork pool with a frozen heap, the officially supported free-threaded build with its single-threaded penalty and ABI risk, or isolated interpreters that share nothing.
## Immortal objects Python 3.12 implemented PEP 683. A small, fixed set of objects is marked immortal by giving its reference count a saturated value that the increment and decrement paths recognise and skip: `None`, `True`, `False`, `Ellipsis`, the cached small integers, and the statically allocated type objects and singletons the interpreter creates for itself. Nothing about how you use them changes. What changes is that using them no longer writes to memory. Copy-on-write friendliness for pre-fork servers was one of the explicit motivations for the PEP, alongside making per-interpreter state possible. Before 3.12, a worker that did nothing but loop over integers and compare against `None` would steadily dirty the pages holding those objects — which, because the allocator packs interpreter startup objects together, are pages holding a great deal of other interpreter furniture too. After 3.12 those pages genuinely stay shared across every worker for the life of the pool. The limit is the part worth being precise about in an interview. Immortality is not something your objects get. A parsed table of a hundred thousand rows is a hundred thousand ordinary objects with ordinary reference counts, and they erode exactly as before. Immortal objects removed a fixed overhead; they did not change the shape of the curve for application data. ## The free-threaded build The free-threaded build first shipped experimentally in 3.13 and is officially supported as of Python 3.14 under PEP 779, at a cost of roughly 5–10% single-threaded performance. It is a separate build of the interpreter, not a switch you flip on the standard one, and it changes reference counting substantially: - **Biased reference counting.** Each object has an owning thread; that thread updates a local count without atomics, while other threads update a shared count atomically. Cheaper, but still a write. - **Deferred reference counting** for some long-lived objects such as modules, classes and top-level functions, which removes a lot of the hottest traffic entirely. - **A wider object header**, carrying the owner thread and the split counts. Net effect on fork: reference counting still writes per-object state on most access, so a forked child still erodes its inherited pages. The free-threaded build does not fix copy-on-write; it makes copy-on-write beside the point. ## Why it changes the question rather than the answer Pre-fork worker pools exist in Python largely to buy CPU parallelism without the GIL while starting from one copy of expensive data. The first half of that is what the free-threaded build delivers directly, and the second half comes free with it: threads in one process share one heap, so a large read-only table is held exactly once, permanently, with no page-copy decay to measure and no freezing recipe to maintain. For a service whose whole design pressure was "we load 4 GB of reference data and we cannot afford sixteen copies of it", that is a materially better answer than anything copy-on-write can offer. The costs are equally concrete and belong in the same answer: the single-threaded penalty; the requirement that your own code actually be thread-safe, since the GIL was doing incidental work for you; and extension-module compatibility, because native dependencies must be built for the free-threaded ABI before they can be loaded at all. On a service with a heavy native dependency stack that last constraint usually decides the matter. ## The third option Multiple interpreters landed in the standard library in 3.14 under PEP 734, with `concurrent.interpreters` and an interpreter-backed pool executor. They give parallelism with far stronger isolation than threads — but they explicitly do *not* share objects, so from the memory-sharing perspective they resemble processes more than threads. If the goal is one copy of a large dataset, interpreters are not the tool; if the goal is isolation with cheaper startup than a process, they are. ## How to answer this well Name the versions, and separate the two mechanisms cleanly. Immortal objects (3.12) are a bounded, real improvement to fork's sharing that applies to interpreter-owned objects only. The free-threaded build (3.14, PEP 779) does not improve fork's sharing at all; it removes the reason you were forking. A candidate who conflates the two, or who claims the free-threaded build eliminated reference counting, has read the headline and not the mechanism.
- Does immortality apply to objects your own startup code creates?No. It covers a fixed set of interpreter-owned objects — `None`, `True`, `False`, the small integers, statically allocated types and singletons. A table your code parses at startup is ordinary objects with ordinary reference counts, so it erodes after a fork exactly as it did before 3.12. Immortality removed a fixed overhead, not the curve.
- What has to be true of a service before the free-threaded build is a realistic option?Its native dependencies must be built for the free-threaded ABI, since incompatible extension modules simply cannot be loaded. Its own code has to be genuinely thread-safe, because the GIL was providing incidental serialization. And the workload must be parallel enough to absorb the 5-10% single-threaded penalty that PEP 779 documents for 3.14.
Immortal objects are the interpreter's road signs: everyone reads them, nobody writes on them, so one copy serves the whole city.
saying these in an interview costs you the question
- Says every object became immortal in Python 3.12
- Thinks the free-threaded build removed reference counting
- Claims immortality keeps application data shared after fork
- Describes free threading as the default build in 3.14
- Expects the free-threaded build to be faster single-threaded