What Are Immortal Objects in Python?
Immortal objects are a core memory management feature introduced in Python 3.12 via PEP 683 that permanently bypass Python’s standard reference counting and garbage collection mechanisms. By design, an immortal object remains in memory throughout the entire lifecycle of the Python runtime, with a reference count that never changes regardless of how many times it is referenced or dereferenced. This article explains the fundamentals of immortal objects, the engineering problems they solve, and their crucial role in modernizing Python's multi-core performance.
How Python Memory Management Traditionally Works
Standard Python (CPython) primarily manages memory through reference
counting. Every standard object in Python contains a header field called
ob_refcnt. When an object is referenced, this counter
increments; when a reference goes out of scope or is deleted, it
decrements. When the counter reaches zero, Python deallocates the
object's memory immediately.
While this system is simple and deterministic, it creates a fundamental limitation: even purely read-only operations must modify the object's internal state to update the reference counter.
What Are Immortal Objects?
Immortal objects are objects whose reference counts are treated as immutable by the runtime. CPython achieves this by setting a specific bit flag or assigning an extraordinarily large value to the object’s reference count field that signals to the runtime to skip incrementing or decrementing operations.
Because their reference counts never change:
- They are never deallocated during runtime.
- They are excluded from cycle detection and garbage collection sweeps.
- Operations that use them behave as true read-only access.
Examples of immortal objects in current versions of Python include
global singletons like None, True,
False, empty tuples, small integers, and internal interned
strings.
Why Immortal Objects Were Introduced
Immortal objects were introduced primarily to solve architectural bottlenecks related to scalability, performance, and concurrency.
1. Enabling True Shared Memory Across Runtimes
Prior to immortal objects, sharing common objects (like
None or small numbers) across multiple threads or isolated
subinterpreters was unsafe. Because reading an object altered its
reference count, two threads accessing the same object simultaneously
would cause a data race on ob_refcnt, requiring locks or
thread-specific copies. Immortal objects make singletons truly
immutable, enabling multiple interpreters or threads to safely share the
same memory without locks.
2. Fixing Copy-on-Write (CoW) Inefficiencies
When Python uses Unix process forking (common in web servers like Gunicorn or Celery), child processes initially share the parent process's memory pages via Copy-on-Write. However, under traditional reference counting, merely reading global objects in a child process altered their reference counts. This caused the operating system to duplicate the entire memory page containing the object, leading to massive memory bloat. Immortal objects prevent reference count mutations, keeping memory pages clean and dramatically reducing the real memory footprint of forked applications.
3. Maximizing CPU Cache Efficiency
Constantly writing to reference count fields invalidates CPU caches, even when an algorithm is only reading data. Immortal objects preserve CPU cache lines by eliminating unnecessary writes, leading to minor baseline execution speedups in cache-sensitive workloads.
4. Paving the Way for the Free-Threaded Build (No-GIL)
The introduction of immortal objects is a foundational prerequisite for PEP 703 (the removal of the Global Interpreter Lock). In a free-threaded Python environment, ubiquitous objects accessed by dozens of threads simultaneously would otherwise suffer severe lock contention or require atomic operations that degrade performance. Making universal objects immortal eliminates synchronization overhead entirely for those objects.