skip to content

Why does `sys.getrefcount(None)` return a huge constant on CPython 3.14?

level: middleimportance: nice to knowfreq 16%

answer

  1. The number never changes, whatever you bind
  2. Not an overflow, a deliberate marker
  3. Some objects outlive every possible decrement
  4. Shared counters were dirtying forked pages
  5. PEP 683 landed in CPython 3.12

basics

~20 s

Since CPython 3.12, interpreter singletons such as None, True, False and the empty tuple are immortal: their reference count is pinned to a sentinel value in the billions and increments and decrements skip them entirely, so the number is not a count of anything.

solid answer

~40 s

PEP 683 introduced immortal objects in CPython 3.12. Objects that must live for the whole process — `None`, `True`, `False`, `Ellipsis`, the empty tuple and other statically allocated singletons — have their count set to a sentinel that reference-counting operations recognise and leave alone, so they can never reach zero and are never deallocated. On a 64-bit 3.14 build `sys.getrefcount(None)` returns 3221225472 and never moves, whatever you bind. The motivation was performance and isolation: constantly writing to those shared counters dirtied copy-on-write pages in forked children and caused cache-line contention between threads, which stood in the way of a per-interpreter GIL in 3.12 and of the free-threaded build. The practical consequence is that a refcount reading on a singleton is meaningless.

code

python · 6 lines
python
import sys

print(sys.getrefcount(None) > 1_000_000)
n = None
print(sys.getrefcount(None) > 1_000_000)
print(sys.getrefcount(None) == sys.getrefcount(True))

go deeper

for a junior

You are not expected to know this. If it comes up, the safe answer is that some interpreter-owned objects live for the whole process, so their reference count is a fixed marker rather than a real count.

for a middle

Explain the mechanism: a sentinel count that increment and decrement operations recognise and skip, so the object can never reach zero. Know that it arrived in CPython 3.12 and that it makes count readings on singletons meaningless.

for a senior

Give the motivation as well as the mechanism — shared counters dirtied copy-on-write pages in forked workers and contended cache lines between threads — and connect it to the per-interpreter GIL and the free-threaded build. Note that it invalidates absolute-count assertions.

for a principal

Use it as evidence for a broader position: interpreter-internal counters are not a stable interface, and anything built on them breaks across versions and build configurations. Steer diagnostics and tests towards observable lifetime and allocation measurements instead.

## The observation ```python import sys print(sys.getrefcount(None)) # 3221225472 on a 64-bit CPython 3.14 build n = None print(sys.getrefcount(None)) # unchanged ``` The number is not an overflow and not a bug. It is a **sentinel**, and the fact that it never moves when you bind another name to `None` is the point. ## Immortal objects, PEP 683, CPython 3.12 A handful of objects are guaranteed to exist for the lifetime of the interpreter: `None`, `True`, `False`, `Ellipsis`, `NotImplemented`, the empty tuple, and a set of other statically allocated singletons. Before 3.12 they were reference-counted like everything else, even though the count could never usefully reach zero. **PEP 683, shipped in 3.12, marks such objects immortal:** their count field is initialised to a special large value, and the interpreter's increment and decrement operations check for that value and do nothing. The object can therefore never be deallocated, and its counter never changes — which is exactly what `sys.getrefcount` faithfully reports back. ## Why anyone bothered The change looks cosmetic and is not. Reference counting *writes* to the object header on every acquire and release, and `None` is touched constantly by every piece of running code. That created two real costs: **Copy-on-write breakage.** A process that forks shares its memory pages with the child until one of them writes. Because every child immediately began incrementing and decrementing the shared singletons, the pages holding those objects were dirtied and copied in every child. Deployments that forked worker processes to share a warm heap paid for it in resident memory. Immortalising the singletons keeps those pages read-only and genuinely shared. **Contention between threads.** Multiple threads hammering the same counter on the same cache line serialise on that line. That is tolerable when a global lock already serialises bytecode execution, and intolerable once you remove it. Immortal objects were a prerequisite both for the per-interpreter GIL that landed in 3.12 and for the free-threaded build — experimental in 3.13, and **officially supported in 3.14 under PEP 779**, where a single-threaded penalty of roughly 5–10% is documented. ## What it means for your code Three concrete consequences: 1. **Never read anything into a singleton's count.** `sys.getrefcount(None)` does not tell you how many names point at `None`; it tells you the object is immortal. Any leak check, test assertion or diagnostic based on such a reading is measuring a constant. 2. **Absolute-count assertions are not portable.** They already had to account for the call's own argument reference; now they must also account for immortality, and in a free-threaded build for counting schemes — deferred and biased reference counting — that are deliberately not a naive shared integer. Assert on observable lifetime instead. 3. **Extension code cannot assume decref frees.** Native code that releases a reference to a singleton is a no-op by design; the memory is never returned. ## What is not immortal Ordinary objects — your instances, lists, dicts, strings you build at runtime — are counted normally and freed the moment their count reaches zero, exactly as before. Immortality applies to a fixed, small set of interpreter-owned objects whose memory cost is bounded and paid once at startup. It is not a leak, and it is not a general opt-in mechanism exposed to Python code. ## How to talk about it in an interview Nobody's offer turns on this. It is the kind of thing an interviewer asks out of curiosity, usually after a broader reference-counting question, to see whether you have kept up with the runtime. A complete answer is three sentences: the value is a pinned sentinel, immortality arrived in 3.12 via PEP 683, and the reason was to stop shared singletons from dirtying copy-on-write pages and contending on cache lines — which was a precondition for a per-interpreter GIL and for free-threading.

  • Does making these objects immortal mean the interpreter leaks memory?
    No. The set is small, fixed and allocated once at startup, and those objects were going to live for the whole process anyway; skipping their counter updates simply removes pointless writes. The memory cost is bounded and does not grow with workload, which is the difference between an intentional lifetime and a leak.
  • How does immortality help a process that forks workers?
    Forked children share the parent's pages until they write to them. Previously every child incremented and decremented the shared singletons immediately, dirtying those pages and forcing a private copy in each worker. With the counters pinned, the pages stay clean and genuinely shared, so resident memory per worker drops.
  • Does the free-threaded build change reference counting further?
    Yes. The free-threaded build — experimental in 3.13, officially supported in 3.14 under PEP 779 — uses deferred and biased reference counting internally rather than one naively shared integer, so a per-object count is even less of a plain ledger. It also carries a documented single-threaded penalty of roughly 5–10%.

saying these in an interview costs you the question

  • Reads the value as the number of names bound to None
  • Calls the number an integer overflow or a display bug
  • Describes immortal objects as a memory leak
  • Thinks releasing a reference could ever free None
  • Claims immortality only exists in free-threaded builds
  • Dates the change to the free-threading release rather than 3.12

context