Python 3.14 garbage collection rigamarole

eatonphil2 pts1 comments

Python 3.14 garbage collection rigamarole - The Consensus

The Consensus Weekly<br>Join the free weekly newsletter to learn about new articles, jobs, and funding.Subscribepython<br>Python 3.14 garbage collection rigamarole<br>Python 3.14.0 introduced a new incremental garbage collector. But reports of higher memory usage caused the Python team to revert the garbage collector changes in 3.14.5.

We investigate how memory management works in Python and workloads that perform best and worst for the incremental garbage collector.By Phil Eaton·June 6, 2026<br>You are getting early access to this article as a subscriber. Your support makes articles like this possible. Thank you.Python 3.14.5 was just released and is the current latest stable version of Python. Python 3.14.0 (released in October, 2025) changed the garbage collector (GC) from traditional generational garbage collection to incremental garbage collection. We’ll get into what this means in this article.From the pull request merging this change into Python:The cycle garbage collector is now incremental. This means that maximum pause times are reduced by an order of magnitude or more for larger heaps.<br>There are now only two generations: young and old. When gc.collect() is not called directly, the GC is invoked a little less frequently. When invoked, it collects the young generation and an increment of the old generation, instead of collecting one or more generations.<br>This pull request made it into Python’s main branch in 2024 but was removed from the 3.13 release branch. Python 3.14.0 was the first release that included this change.But users reported “memory pressure” so the Python team reverted changes to the garbage collector in the 3.14.5 release. We’ll get into what “memory pressure” means in this article as well.Unfortunately the garbage collection changes were somewhat intentionally (if that’s not too strong to say) not implemented as an alternative that users could switch between (which, for example, you can do in Java or Go). Users who liked the new incremental garbage collector (and they exist) can no longer use it at all. Also interestingly, the GC changes did not go through the usual PEP process in the first place.To understand what all of this means though, we need to start before the GC: with reference counting.Throughout this post I’ll say “Python” when I am only necessarily talking about “CPython”.Python#<br>Before we get into reference counting, let’s build two versions of Python locally: 3.14.4 and 3.14.5. Since we cannot switch between which GC we use within a single version, the best we can do (as we demonstrate behavior throughout this post) is to switch between these two patch versions. 3.14.4 has the incremental GC and 3.14.5 has the traditional GC.sudo apt-get update -y<br>sudo apt-get install -y build-essential pkg-config<br>git clone --depth 1 --branch v3.14.4 https://github.com/python/cpython cpython3.14.4<br>git clone --depth 1 --branch v3.14.5 https://github.com/python/cpython cpython3.14.5<br>(cd cpython3.14.4 && ./configure --with-trace-refs && make -j16)<br>(cd cpython3.14.5 && ./configure --with-trace-refs && make -j16)

The --with-trace-refs enables an additional debug method we’ll talk about later.So now you’ve got both versions.$ ./cpython3.14.4/python --version<br>Python 3.14.4<br>$ ./cpython3.14.5/python --version<br>Python 3.14.5

Let’s get into memory management!Reference counting primer#<br>Objects in Python are reference counted. New references to an object increment the count. The count is decremented in a few scenarios. For example: when a variable goes out of scope, or a variable is del-ed, or a variable is bound to a different object.We can observe reference counts through sys.getrefcount.import sys<br>print(sys.getrefcount([])) # 1

refcount1.pyRun it.$ ./cpython3.14.4/python refcount1.py<br>$ ./cpython3.14.5/python refcount1.py

In this case we created a new object and there is only a single reference to it. It will get deallocated as soon as sys.getrefcount() completes because its reference count goes to 0.Variables create additional references.import sys<br>a = []<br># refcount for this object is 1<br>print(sys.getrefcount(a)) # 2: `a` and a temp reference passed to `sys.getrefcount`

refcount2.pyRun it.$ ./cpython3.14.4/python refcount2.py<br>$ ./cpython3.14.5/python refcount2.py

Multiple variables pointing at the same object create multiple references. We can print id(obj) (which in CPython is the actual memory address of the object) on multiple variables pointing to the same object and observe that the ids are the same. Python reference counting acts on the object, not the variable.import sys

a = []<br>print("a memory", hex(id(a))) # 0x1026e1680 in a run on my machine<br># 0x1026e1680 refcount is 1: `a`<br>print(sys.getrefcount(a)) # 2: `a` itself and the argument to sys.getrefcount

b = a<br>print("b memory", hex(id(b))) # same as `a memory`, 0x1026e1680 in the same run<br># 0x1026e1680 refcount is 2: `a`, and `b`<br>print(sys.getrefcount(a)) # 3: `a` itself, `b` and the...

python garbage cpython3 memory reference object

Related Articles