Deep Engineering
Search
Expert·Published·3.12 · 3.13 · 3.14·35 MIN

Memory management in CPython: three numbers everyone repeats wrong

Garbage collector thresholds, arena size, the cost of an object — none of it is what the articles say any more. Every number here was taken off a running interpreter and checked against the source at a pinned tag.

Full technical treatment

TL;DR

  • Memory in CPython is released by two mechanisms, not one: reference counting does it immediately, and the cycle collector does it later and only for what the counter cannot reach.
  • The collector thresholds (700, 10, 10), quoted everywhere, went stale in 3.13. They are now (2000, 10, 10), and along the way they were (2000, 10, 0).
  • 256 KB arenas, 4 KB pools and 64 size classes describe a 32-bit build. On a 64-bit build it is 1 MiB, 16 KiB and 32 classes, because alignment there is 16 bytes, not 8.
  • In the build without the GIL the object header grows from 16 to 32 bytes, and pymalloc is replaced by mimalloc. The price shows up in sys.getsizeof — but not for every object, and the reason for that is instructive in itself.

Why know this?

Because almost everything written about memory in Python describes an interpreter you no longer have.

That is not an exaggeration. Below are three numbers that turn up in every other article, next to what the interpreter actually returned when checked. All three disagree with the received wisdom, and they disagree recently: two of them changed in 3.13, and the third has depended on the build's word size for a long time, yet keeps circulating in its old form.

The practical value is not in the numbers themselves but in the habit. Memory is the area where "I read that" goes out of date most quietly: gc.get_threshold() returns a tuple with no warning that it was something else the day before yesterday, and sys.getsizeof does not mention that the answer differs by a factor of two in the build next door.

The mental model

Picture a library where every book has a number on its cover saying how many readers are holding it right now. A reader takes it — add one; returns it — subtract one. The moment the number hits zero, the book goes straight to the pulper: not at the end of the day, not on a schedule, but right then.

The scheme works flawlessly up to exactly one case. Two books, each with a bookmark pointing at the other. Both readers have left, but the numbers are not zero: the books are holding each other. By the library's rules they are alive; in fact nobody will ever open them again. It is for this situation — and only this one — that a second mechanism exists, one that walks the shelves every so often looking for closed groups that cannot be reached from outside.

Everything else in this article is the detail of those two mechanisms and the price paid for them.

Reference counting: the main event, not the fallback

Start with what usually gets mentioned in passing: the garbage collector is not doing most of the work. The vast majority of objects in Python are freed by the reference count, immediately, and the collector never hears about them.

Every object begins with an ob_refcnt field. The Py_INCREF and Py_DECREF macros raise and lower it, and when Py_DECREF brings the count to zero, the deallocation happens right there, inside that call.

step 1 of 6
  1. a = Thing("payload")
  2. b = a
  3. box = [a]
  4. del b
  5. box.clear()
  6. del a

the object is created, a single name points at it

ob_refcnt

1

held by

  • a

the measurement disturbs what it measures
The same data taken two ways on one object: sys.getrefcount(x) - 1 written inline gave 1, and through a helper function — 2. A function parameter is a reference too. That is why the expression is written inline throughout the article instead of being moved into a convenient rc().

Fig. 1. The counter values are the output of a run on CPython 3.13.13, not an illustration. Note the last step: the object is freed at the very moment the counter hits zero — this is done by reference counting itself, not by the garbage collector.

Look at the bottom strip of the diagram. It covers something that looks like a footnote but explains half the confusion around getrefcount: the measurement itself adds a reference. An argument passed to a function is a reference, so sys.getrefcount(x) is always one higher than the "real" value. And if you write a convenient wrapper, def rc(obj): return sys.getrefcount(obj) - 1, the interference grows to two: the parameter obj is another reference. Measured on 3.13.13: inline the expression gives 1, through the wrapper 2.

The conclusion is not "getrefcount lies" but something more general: this instrument has an observation cost, and you have to subtract it deliberately.

A weak reference is a reference that does not count

Sometimes an object needs to be remembered without being held: a cache, a registry of observers, a back-reference from child to parent. That is what weakref is for — a reference that does not increment ob_refcnt.

PYTHON
import sys, weakref
 
class T: pass
 
t = T()
sys.getrefcount(t) - 1        # 1
r = weakref.ref(t)
sys.getrefcount(t) - 1        # 1 — the weak reference was not counted
r() is t                      # True
 
del t
r()                           # None — the object is gone, and the reference knows it

The numbers in the comments are this code's output on 3.13.13. The last line is the important one: the weak reference does not turn into a dangling pointer, it honestly starts returning None.

Cycles: the one thing counting cannot do

Now for that pair of books with bookmarks pointing at each other.

left = Node('left')
right = Node('right')
leftrightNode('left')ob_refcnt = 1Node('right')ob_refcnt = 1

two ordinary objects, one reference each

Freed:

Fig. 2. The numbers were taken on CPython 3.13.13 in a run with real __del__ methods: after del of both names nothing was freed, while gc.collect() returned 2 and invoked both destructors. Reference counting is not wrong here — it answers its own question correctly. Its question (“does the object have at least one reference”) simply is not the question that matters (“is the object reachable from the running program”).

The numbers in the diagram are worth reading carefully, because this is usually where the confusion starts. After del left; del right the counts are not zero and not "undefined" — they are one. Each object is holding the other, and from the counter's point of view this is indistinguishable from a live data structure.

Reference counting is not making a mistake here. It answers its own question correctly — "does this object have at least one reference?" The question that matters is a different one: "is this object reachable from the running program?" The gap between those two questions is the entire reason CPython has a second collector.

In the measurement gc.collect() returned 2 and ran both __del__ methods. Before that, neither had run.

Thresholds: the number everyone remembers wrong

The cycle collector does not run continuously. It wakes up when the difference between the number of tracked objects created and the number destroyed exceeds a threshold.

What these three numbers actually count

Before sorting out which numbers are right, it is worth knowing what they configure: without that, (700, 10, 10) looks like three values of one kind, and it is not.

There are three generations, and they are three lists. Every tracked object sits in exactly one of them. A freshly created object lands in generation 0. One that survives a collection of its own generation moves up to the next — and from then on is collected less often.

The point of the split is cost, not tidiness. A full traversal of the heap takes time proportional to its size, while the overwhelming majority of objects die almost immediately after being created: a temporary list inside a loop, an unpacked tuple, an intermediate string. Walking the entire heap for their sake, including structures that have been alive since the process started, means paying to check what has almost certainly not changed. Generations let you look often and cheaply where mortality is high, and rarely where it is low.

And now the thing that makes the three numbers look alike. They measure different quantities:

ThresholdWhat its counter countsWhen the counter grows
threshold0objectsevery time a tracked object is created
threshold1collections of generation 0every time generation 0 is collected
threshold2collections of generation 1every time generation 1 is collected

So (2000, 10, 10) reads like this: collect generation 0 after 2000 new objects; on every tenth such collection, sweep generation 1 in as well; on every tenth collection of generation 1, sweep in generation 2. The first number is about objects, the other two about collections. Hence the difference in magnitude: 2000 against 10 is not "one threshold is far bigger than the others", it is two different units.

The ambiguity is baked into the source itself — in the generation struct a single count field serves both cases, and its comment honestly lists both:

C
struct gc_generation {
    PyGC_Head head;
    int threshold; /* collection threshold */
    int count; /* count of allocations or collections of younger
                  generations */
};

The selection machinery fits into one loop — this is gc_select_generation() from Python/gc.c at tag v3.13.0:

C
for (int i = NUM_GENERATIONS-1; i >= 0; i--) {
    if (gcstate->generations[i].count > gcstate->generations[i].threshold) {
        if (i == NUM_GENERATIONS - 1
            && gcstate->long_lived_pending < gcstate->long_lived_total / 4)
        {
            continue;
        }
        return i;
    }
}

The loop runs from the oldest generation down to the youngest and takes the first one whose counter has passed its threshold. Having settled on generation i, the collector walks it together with every younger generation, zeroes their counters and adds one to the counter of the generation above:

C
if (generation+1 < NUM_GENERATIONS) {
    gcstate->generations[generation+1].count += 1;
}
for (i = 0; i <= generation; i++) {
    gcstate->generations[i].count = 0;
}

And right there — a condition that appears neither in the documentation nor in the retellings. The line with long_lived_pending means that for the oldest generation, passing the threshold is not enough. The full collection is skipped if the number of objects that have survived every partial collection and have not yet been examined by a full one is smaller than a quarter of the number that survived the last full one. In plainer terms: if barely any long-lived objects have piled up since the last full traversal, there is no reason to traverse everything again.

The practical consequence matters more than the condition itself: threshold2 = 10 is a necessary condition for a full collection, not a sufficient one. A program whose population of long-lived objects settled long ago can go through hundreds of generation-1 collections without seeing a single full one. The claim that "a full collection happens every hundred generation-0 collections" is arithmetically tidy and false.

Now the numbers

There are three thresholds, and in almost every article they are given as (700, 10, 10). Checked on five interpreters:

Buildgc.get_threshold()
3.11.15(700, 10, 10)
3.12.3(700, 10, 10)
3.13.13(2000, 10, 10)
3.14.0rc2(2000, 10, 0)
3.14.5(2000, 10, 10)

This is not a quirk of particular builds. In Include/internal/pycore_runtime_init.h at tag v3.12.0 the line reads { .threshold = 700, }; in the same file at v3.13.0 it is already 2000.

Separately curious: the gc documentation does not describe the default values at all — checked for 3.13 and 3.14. The page explains what the three thresholds mean but gives no numbers. Where exactly 700 came from before spreading through the articles, I will not claim to know. Something else is certain: the number has no confirmation in the documentation, while gc.get_threshold() always does — and it answers for the build you are running.

Checking the threshold in effect takes ten lines:

PYTHON
import gc
 
gc.collect()
first = gc.get_threshold()[0]
gen1 = gc.get_count()[1]
 
keep = []
for i in range(1, 4001):
    keep.append([i])              # a list is tracked by the collector
    if gc.get_count()[1] != gen1: # generation 0 has been collected
        print(first, i)
        break

On 3.13.13 this prints 2000 2000, on 3.12.3 700 700, and it did so on five runs out of five: collection fires on exactly the object whose number equals the threshold.

One caveat: on 3.14.0rc2 the same code prints nothing across 4000 objects. There the third threshold is zero, collection proceeds in increments, and the generation counters behave differently — which is itself a decent illustration of the next section.

The revert story, worth knowing in full

The third number in the 3.14.0 row is zero, and that is not a typo.

In 3.13.0a6 an incremental cycle collector landed in CPython. The changelog wording: an incremental collector is implemented, the old generation is collected in chunks, a full heap traversal is no longer needed, and the number of generations drops from three to two. The point was to remove long pauses on large heaps.

It did not make the 3.13 release; it made 3.14.0. At the same time the third threshold stopped meaning anything: the 3.14 documentation marks threshold2 as ignored and gc.collect(1) as "perform an increment of collection" rather than "collect generation 1". Hence the (2000, 10, 0) measured on 3.14.0rc2.

And then — a rare thing — the change was reverted in a patch release:

Python 3.14.0-3.14.4 shipped with a new incremental GC. However, due to a number of reports of significant memory pressure in production environments, it has been reverted back to the generational GC from 3.13.

What's New In Python 3.14, Garbage collection section

The 3.14.5rc1 changelog entry (gh-142516) adds that the revert was done for the default build, and that the free-threaded build's collector was left unchanged. That does not mean the incremental collector survives there: since 3.13.0a4 the build without the GIL has had its own collector implementation, which finds objects through mimalloc and, in that same changelog's wording, is not generational. The incremental collector never touched it, before the revert or after.

The practical takeaway for anyone running large heaps: "Python 3.14" in a requirements line is not precise enough. Collector behaviour differs between 3.14.3 and 3.14.5, and the difference is exactly what the revert was about.

Where the objects actually live: arenas, pools, blocks

The second mechanism is not collection but allocation. Asking the operating system for memory for each object separately is far too expensive: Python objects are small and get created by the million. So CPython takes memory in large chunks and hands it out itself.

Three levels: arenapoolblock.

Size classes: why there are 32, and what they are for

This part usually gets one passing phrase — "objects are sorted into size classes" — and the reader is left with the sense that the classes just happen to exist. Yet they are precisely what makes allocation cheap, and they are worth understanding before any of the numbers.

A size class is the set of requests that all get a block of the same size. A request is rounded up to the nearest multiple of sixteen: 1 byte and 16 bytes both get a 16-byte block; 17 and 32 both get a 32-byte one; and so on up to 512. Hence 32 classes: 512 ÷ 16.

Sixteen is the ALIGNMENT of a 64-bit build, and the number of classes follows from it directly. On a 32-bit build the alignment is eight, and there are 64 classes. The article comes back to this in more detail; what matters here is that "32" is not a property of pymalloc as such but of the build every number in this article was measured on.

In the source, working out the class is a single line of arithmetic — the number falls out of a shift:

C
uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;

The crucial part is the rule the whole design exists for: one pool serves exactly one class. Every block inside a pool is the same size, which makes them interchangeable. And that reduces allocation to two operations on a singly linked list:

  • allocate — take the first block off this pool's free list;
  • free — put the block back at the head of the list.

No hunting for a hole that fits, no splitting a large chunk into a smaller one, no coalescing adjacent free regions — none of the work a general-purpose malloc is stuck with. It is stuck with it precisely because requests of every size arrive mixed together. pymalloc buys its way out of that work with rounding: part of a block goes to waste, but an allocation costs a handful of instructions. How much goes to waste is something the interpreter counts for you — the figure appears below, in the walk-through of sys._debugmallocstats().

So the answer to "why are the classes different sizes" is: so that everything inside a single pool is the same. The variety is pushed one level up — there are many pools, and each is busy with a size of its own.

How memory fills up and how it is released

Filling goes from the top down, and only as needed:

  1. The program asks for 100 bytes. Rounding up gives the 112-byte class.
  2. The allocator looks for a pool of that class with a free block in it and takes a block off the free list.
  3. If there is no such pool, an empty pool is taken (from an arena already on hand) and assigned to class 112. A pool is not born with a size; it acquires one on first use.
  4. If no empty pools are left, a new 1 MiB arena is requested from the operating system and carved into 64 pools.

Releasing runs through the same three levels, but each level up demands a stronger condition:

What is releasedConditionWhere it goes
blockthe object was deletedonto its own pool's free list — instantly, with no involvement from the OS
poolall of its blocks became freeinto the arena's stock of empty pools, and may be reassigned to another class
arenaall of its pools became freeback to the operating system

That asymmetry is the whole reason for Python's reputation for "not giving memory back". Downwards, memory moves instantly and one block at a time; upwards, only in whole levels and only on complete emptiness. One live object holds its pool; one non-empty pool holds a megabyte of arena.

All three rules are easier to watch than to read about: the first tab shows what happens to a request before memory is handed out, the second shows a block being reused inside a pool, the third shows why a process will not give up a megabyte because of a single live object.

requested
100 B
class 6
112 B

The request is rounded up to a multiple of 16. The block is 12 B larger than asked for — the price of every block in a pool being identical.

Fig. 4. A schematic of the algorithm, not a recording of a real heap. Only the constants are measured: arena 1,048,576 B, pool 16,384 B (64 pools per arena), 32 classes in steps of 16 B up to and including 512 B — the output of sys._debugmallocstats(), identical on 3.11.15, 3.12.3 and 3.13.13. Block count and hand-out order are simplified: a real 64-byte pool holds 255 blocks, and by the time your code runs the interpreter already holds thousands in use.

An arena is a contiguous chunk of address space that the interpreter takes from the operating system as a whole. On a 64-bit build its size is 1,048,576 bytes, that is 1 MiB, and it is cut into exactly 64 pools.

64 pools of 16,384 bytes each. The fill shown in the diagram is an illustrative layout; for the exact occupancy figures see the “Block” level — those are measured.

Fig. 3. The numbers come from parsing the output of sys._debugmallocstats() on CPython 3.13.13 at the end of the measurement script run. The sizes and the number of classes are build constants; occupancy is a snapshot of this process. The constants were checked against Include/internal/pycore_obmalloc.h at tag v3.14.5: ARENA_BITS 20, POOL_BITS 14, ALIGNMENT 16, SMALL_REQUEST_THRESHOLD 512. If you remember “256 KB and 4 KB” — that holds for a 32-bit build, where the same constants are 18 and 12.

This is where the second stale number lives. The classic description — "256 KB arenas, 4 KB pools, 8-byte alignment, 64 size classes" — is correct for a 32-bit build. On 64-bit, ARENA_BITS is 20 and POOL_BITS is 14, that is 1 MiB and 16 KiB; alignment is 16 bytes and there are 32 size classes. The interpreter says so itself:

Small block threshold = 512, in 32 size classes
2 arenas * 1048576 bytes/arena     =            2,097,152
23 unused pools * 16384 bytes      =              376,832

That is the output of sys._debugmallocstats() on a bare 3.13.13 startup. It has to be read in two different ways. The first line and the arena and pool sizes are build constants, identical from run to run. The number of arenas and pools, though, is the state of one process at one moment; three consecutive runs gave 2 arenas and 22, 23, 23 pools. There is a size-class occupancy table later in this article, and the same caveat applies to it in full.

As for the "64 size classes" of the classic descriptions — the figure is right, and it is genuinely about classes: on a 32-bit build ALIGNMENT is 8, and 512 ÷ 8 does give 64 classes. On 64-bit the alignment is 16, so there are half as many classes. The coincidence with the number of pools in an arena (1 MiB ÷ 16 KiB = 64) is just a coincidence, and the two sixty-fours should not be confused.

Two traps if you want to repeat the measurement. First, sys._debugmallocstats() prints to stderr, not stdout. Second — and this is not obvious — it prints from C, so contextlib.redirect_stderr does not capture it: the substitution works at the level of the sys.stderr object, not the file descriptor. The capture "succeeds", returns an empty string, and your parsing silently produces nothing. You need os.dup2.

What follows from this in practice

A request of up to and including 512 bytes goes to pymalloc and is rounded up to the nearest of the 32 classes. Anything larger goes straight to the system malloc. The boundary is exactly there: in obmalloc.c the condition is written as nbytes > SMALL_REQUEST_THRESHOLD, so a request of exactly 512 bytes is still served by pymalloc — the last class in the table, class 31.

This explains two things that otherwise look odd. Rounding up to a size class is the "quantization" you hear about: in that same bare-startup output it cost 7,552 bytes. And an arena is returned to the operating system only once it is completely empty — and not even always then: the allocator holds on to the last free arena rather than give it back and immediately ask for it again. So a long-lived process that once created a million objects and deleted them may not give the memory back: one live object left in each arena is enough.

The price of free-threading, measured with a ruler

Now for the part that cannot be left out of a memory article in 2026.

PEP 703 (Sam Gross, status Final, Python 3.13) removes the GIL. For memory this means three concrete changes, and all three are visible under measurement.

One: the object header doubled. In an ordinary build struct _object is a counter and a pointer to the type. In the build without the GIL it is a different structure entirely: ob_tid (the owning thread's id), ob_mutex (a per-object lock), ob_gc_bits, ob_ref_local (the local counter) and ob_ref_shared (the shared, atomic one). This is biased reference counting: the owning thread touches its local counter without atomic operations, other threads touch the shared one.

The price is visible to the naked eye:

Object3.14 with GIL3.14t without GIL
object()16 B32 B
1.524 B40 B
empty dict64 B64 B
empty list56 B56 B

Sixteen bytes are added to every object that has nothing beyond its header. Why list and dict weigh the same in both builds is the more interesting question, and "it got lost in the alignment" is the wrong answer.

The reason is different. An object tracked by the cycle collector carries, in an ordinary build, a PyGC_Head structure in front of it — two pointers, 16 bytes, and sys.getsizeof counts them. In the build without the GIL that structure does not exist at all: the collector's state moved into the ob_gc_bits field inside the header itself. The result is an even trade: the header grew by 16 bytes, PyGC_Head disappeared for 16 bytes.

The arithmetic works out on an empty list: 40 bytes of fields plus 16 bytes of PyGC_Head in the GIL build, and 56 bytes of fields with no PyGC_Head at all in the build without it. Either way, 56. For object(), which the collector does not track, there is nothing to offset — there the growth shows in full.

Two: pymalloc is replaced by mimalloc. PEP 703 gives the reason plainly: the pymalloc implementation is not thread-safe without the GIL. The swap shows in that same sys._debugmallocstats() — the two builds speak different vocabularies:

with GIL:     Small block threshold = 512, in 32 size classes
without GIL:  Small block threshold = 16384, in 73 size classes
              Medium block threshold = 131072
              Large object max size = 16777216

There are no "arenas" and no "pools" in the second build at all. It is a different allocator, not the old one configured differently.

Three: there are more immortal objects. Objects that live for the whole run of the program — None, True, small integers, interned strings — get a fixed reference count and stop changing it (PEP 683, Python 3.12). The value of the constant has moved: in 3.12 and 3.13 getrefcount(None) returns 4294967295, in 3.14 it is 3221225472, which is exactly 3ULL << 30 from Include/refcount.h.

More interesting, though, is comparing builds of the same version:

Expression3.14 with GIL3.14t without GIL
getrefcount(None)32212254723221225472
getrefcount(257)33221225472

257 is not a small integer; in an ordinary build it is a perfectly ordinary object with a count of 3. In the build without the GIL the literal 257 written in the code turns out to be immortal.

This has to be phrased carefully or it becomes false. What is immortal is not "the number 257" but a code object's constant: the same value obtained by computation behaves normally.

PYTHON
sys.getrefcount(257)          # 3221225472 — a literal from the code
a = 256; b = a + 1
sys.getrefcount(b)            # 2 — same value, ordinary object

The output was taken on 3.14.0rc2t. The reason is that the free-threaded build interns and immortalizes most code object constants (Objects/codeobject.c, the should_immortalize_constant function) — this removes contention between threads over their counters. The general logic is the same as in PEP 703: immortality eliminates atomic operations on the counter. But the list of objects the PEP declares immortal is the set from PEP 683 (interned strings, small integers, static types, True/False/None), and code object constants are not in it: the implementation added those.

What is a language guarantee and what is an implementation detail

The distinction is fundamental, because nearly everything in this article is the second kind.

ClaimStatus
An object is freed when no references to it remainCPython detail. The language spec promises nothing about when
Cycles are handled by a separate collectorCPython detail
Thresholds (2000, 10, 10)CPython detail, changed in 3.13 and again within 3.14
1 MiB arena, 16 KiB poolCPython detail, depends on the build's word size
Object header of 16 or 32 bytesCPython detail, depends on the build mode
weakref does not keep the object aliveDocumented standard library behaviour
gc.collect() collects cyclesAn interface guarantee, but not about timing or amount

The row about immediate deallocation is the important one. In CPython del last_name really does call __del__ at once, and piles of code involving files and sockets are built on that. But it is a property of reference counting, not of the language: on PyPy or Jython the same code frees the object eventually. That is precisely why with exists.

The same caveat applies to weak references, and the documentation itself states it: weakref guarantees that the reference does not keep the object alive, but it does not guarantee that the reference starts returning None the instant the last strong reference disappears — until the object is actually destroyed, the reference may still hand it back. That r() returned None immediately in the example above is again thanks to reference counting, not a promise from the library.

What to keep in mind

The three numbers in this article will go out of date; the only question is which one goes first. It is more useful to remember how to check them than to remember them: gc.get_threshold() for the thresholds, sys._debugmallocstats() for the allocator, sys.getsizeof alongside sys._is_gil_enabled() for the cost of an object.

And one closing observation. The incremental collector story is about how even a good optimization can fail to survive contact with production: it shipped, it reduced pauses, it drove memory use up, and it was reverted in a patch release. Between 3.14.3 and 3.14.5 you have different garbage collectors. Not one line of your code changed.

Common misconceptions

Claim

“The collector thresholds are (700, 10, 10).”

Actually

They were, until 3.13. Measured: 3.11 and 3.12 give (700, 10, 10), 3.13 gives (2000, 10, 10), 3.14.0 gives (2000, 10, 0), and 3.14.5 is back to (2000, 10, 10). In the sources at tag v3.12.0 the value is 700; at v3.13.0 it is 2000. The awkward part: the default values were never described in the gc documentation, so the change went through quietly.

Claim

“Memory in Python is freed by the garbage collector.”

Actually

Most of the work is done by reference counting, and done immediately: the moment Py_DECREF brings ob_refcnt to zero, the object is freed inside that very call. The cycle collector exists for the single case the counter cannot handle — groups of objects that refer to each other and are unreachable from outside.

Claim

“Objects in a cycle have a reference count of zero, which is why they are not deleted.”

Actually

The opposite: measurement shows that after del of both names the counts are one, not zero. Each object is genuinely holding the other. It is precisely because the counts are non-zero that reference counting considers the pair alive — and a second pass is needed, one that asks not "is there a reference?" but "is the object reachable?"

Claim

“Arenas are 256 KB, pools are 4 KB, and there are 64 size classes.”

Actually

That is a 32-bit build. On 64-bit, ARENA_BITS = 20 and POOL_BITS = 14 (pycore_obmalloc.h, tag v3.14.5), that is 1 MiB and 16 KiB, alignment 16 bytes, 32 size classes. The interpreter prints the sizes itself: 2 arenas * 1048576 bytes/arena, 23 unused pools * 16384 bytes — the sizes there are build constants, while the counts change from run to run. About the "64 classes", don't mix things up: the figure is correct for classes specifically, because on a 32-bit build the alignment is 8 and 512 ÷ 8 = 64.

Claim

sys.getrefcount(x) shows the number of references to an object.”

Actually

It shows one more: the function argument is a reference too. And the size of the interference depends on how you measure: the same value taken through a helper function comes out two higher, because that function's parameter is another reference. Measured on 3.13.13: inline 1, through the wrapper 2.

Claim

“Turning off the GIL is about speed and has nothing to do with memory.”

Actually

It has, and measurably. The object header grows from 16 to 32 bytes: instead of a counter and a type, it holds ob_tid, ob_mutex, ob_gc_bits and two reference counts — local and shared. sys.getsizeof(1.5) gives 24 bytes with the GIL and 40 without it. For objects tracked by the collector the growth is invisible: PyGC_Head disappears at the same time, for the same 16 bytes, so an empty list weighs 56 in both builds. On top of that, pymalloc is replaced by mimalloc — the two builds produce different sys._debugmallocstats() output.

Knowledge check

Question 1 of 5

An object has been created and one name refers to it. What will sys.getrefcount(obj) return if you call it directly on the same line?

Sources & further reading

13 SOURCES

  1. Include/refcount.h — reference counting and immortal objectsCPython source code. The definitions of Py_INCREF/Py_DECREF and the constant _Py_IMMORTAL_INITIAL_REFCNT (3ULL << 30) — the very number getrefcount returns for None. CPython tag 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/refcount.h
  2. Include/object.h — the PyObject layout in both buildsCPython source code. Two different struct _object definitions: one under #ifndef Py_GIL_DISABLED and one under #else. The second is the one with ob_tid, ob_mutex, ob_ref_local and ob_ref_shared. CPython tag 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/object.h
  3. Include/internal/pycore_obmalloc.h — pymalloc constantsCPython source code. ARENA_BITS, POOL_BITS, ALIGNMENT and SMALL_REQUEST_THRESHOLD, plus the comment on why arenas are taken through mmap. CPython tag 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_obmalloc.h
  4. Include/internal/pycore_interp_structs.h — initial GC thresholdsCPython source code. GC_GENERATION_INIT: 2000, 10, 10 for the GIL build, with a separate branch for free-threaded. CPython tag 3.14.5.https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_interp_structs.h
  5. Include/internal/pycore_runtime_init.h (3.12) — thresholds before the changeCPython source code. The same initialization block at tag v3.12.0, where the first threshold is still 700. Needed to show that the number moved. CPython tag 3.12.0.https://github.com/python/cpython/blob/v3.12.0/Include/internal/pycore_runtime_init.h
  6. Python/gc.c (3.13) — picking the generation to collectCPython source code. gc_select_generation(): the loop running from the oldest generation down to the youngest, and the condition long_lived_pending < long_lived_total / 4 that makes exceeding threshold2 insufficient for a full collection. Also here: the zeroing of the collected generations' counters and the increment of the next one's. CPython tag 3.13.0.https://github.com/python/cpython/blob/v3.13.0/Python/gc.c
  7. Include/internal/pycore_gc.h (3.12) — the generation structCPython source code. NUM_GENERATIONS is 3; struct gc_generation carries a single count field whose comment reads "count of allocations or collections of younger generations" — the very ambiguity that makes the three thresholds look like values of one kind. CPython tag 3.12.0.https://github.com/python/cpython/blob/v3.12.0/Include/internal/pycore_gc.h
  8. Objects/obmalloc.c (3.13) — arenas, pools, blocks and size classesCPython source code. Computing the class number with a shift ((nbytes - 1) >> ALIGNMENT_SHIFT), the rule that one pool serves one class (pool->szidx), the pool->freeblock free list, and the condition for handing an arena back to the system — only when all of its pools are free. CPython tag 3.13.0.https://github.com/python/cpython/blob/v3.13.0/Objects/obmalloc.c
  9. What's New In Python 3.14 — Garbage collection sectionOfficial documentation. The direct statement that the incremental collector from 3.14.0–3.14.4 was reverted in 3.14.5 after reports of growing memory use in production.https://docs.python.org/3.14/whatsnew/3.14.html#incremental-garbage-collection
  10. gc module documentation (3.14)Official documentation. What the three thresholds mean, and the "Changed in version 3.14 / 3.14.5" notes. The default values are not documented here — and never were.https://docs.python.org/3.14/library/gc.html
  11. weakref module documentationOfficial documentation. A weak reference does not keep the object alive; once the object is freed, calling the reference returns None.https://docs.python.org/3/library/weakref.html
  12. PEP 683 — Immortal Objects, Using a Fixed RefcountPEP. Eric Snow and Eddie Elizondo, Python 3.12, status Final. Where objects with a fixed reference count came from.https://peps.python.org/pep-0683/
  13. PEP 703 — Making the Global Interpreter Lock Optional in CPythonPEP. Sam Gross, Python 3.13, status Final. Biased reference counting, pymalloc replaced by mimalloc, per-object locks, and immortality as a way to remove contention over the counter.https://peps.python.org/pep-0703/