NOW LET US – AI RAG SaaS Studio TP.HCM
NOW LET US
Digital Product Studio
Back to news
DEV-TOOLS...6 min read

Python 3.14 garbage collection rigamarole

Share
NOW LET US Article – Python 3.14 garbage collection rigamarole

Python 3.14.0 introduced an incremental garbage collector to reduce pause times, but it was reverted in 3.14.5 due to high memory usage. This article explores Python's memory management and why the new GC failed.

Python 3.14 garbage collection rigamarole

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.

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.

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.

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

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
sudo apt-get install -y build-essential pkg-config
git clone --depth 1 --branch v3.14.4 https://github.com/python/cpython cpython3.14.4
git clone --depth 1 --branch v3.14.5 https://github.com/python/cpython cpython3.14.5
(cd cpython3.14.4 && ./configure --with-trace-refs && make -j16)
(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
Python 3.14.4
$ ./cpython3.14.5/python --version
Python 3.14.5

Let’s get into memory management!

Reference counting primer

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
print(sys.getrefcount([])) # 1

refcount1.py

Run it.

$ ./cpython3.14.4/python refcount1.py
1
$ ./cpython3.14.5/python refcount1.py
1

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
a = []
# refcount for this object is 1
print(sys.getrefcount(a)) # 2: `a` and a temp reference passed to `sys.getrefcount`

refcount2.py

Run it.

$ ./cpython3.14.4/python refcount2.py
2
$ ./cpython3.14.5/python refcount2.py
2

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 = []
print("a memory", hex(id(a))) # 0x1026e1680 in a run on my machine
# 0x1026e1680 refcount is 1: `a`
print(sys.getrefcount(a)) # 2: `a` itself and the argument to sys.getrefcount
b = a
print("b memory", hex(id(b))) # same as `a memory`, 0x1026e1680 in the same run
# 0x1026e1680 refcount is 2: `a`, and `b`
print(sys.getrefcount(a)) # 3: `a` itself, `b` and the object as argument
print(sys.getrefcount(b)) # 3: `b` itself, `a`, and the object as argument
del b
# 0x1026e1680 refcount is 1
print(sys.getrefcount(a)) # 2: `a` itself and the object as argument
del a
# 0x1026e1680 refcount is 0, deleted

refcount3.py

Run it.

$ ./cpython3.14.4/python refcount3.py
a memory 0xfed730bdda40
2
b memory 0xfed730bdda40
3
3
2
$ ./cpython3.14.5/python refcount3.py
a memory 0xf1331f9dda40
2
b memory 0xf1331f9dda40
3
3
2

We cannot observe deallocations for most builtin objects (e.g. lists, dicts, etc.) but we can observe deallocations in user objects either by implementing the __del__ method or by assigning a callback with weakref.finalize.

import sys, weakref
class Obj: pass
a = Obj()
weakref.finalize(a, print, "freeing "+hex(id(a)))
print("a memory", hex(id(a))) # 0x1005b4d70 in a run on my machine
# 0x1005b4d70 refcount is 1: `a`
print(sys.getrefcount(a)) # 2: `a` itself and the argument to sys.getrefcount
b = a
print("b memory", hex(id(b))) # same as `a memory`, 0x1005b4d70 in the same run
# 0x1005b4d70 refcount is 2: `a`, and `b`
print(sys.getrefcount(a)) # 3: `a` itself, `b` and the object as argument
print(sys.getrefcount(b)) # 3: `b` itself, `a`, and the object as argument
del b
# 0x1005b4d70 refcount is 1
print(sys.getrefcount(a)) # 2: `a` itself and the object as argument
del a
# 0x1005b4d70 refcount is 0, deleted, observe `freeing 0x1005b4d70` printed

refcount4.py

Run it.

$ ./cpython3.14.4/python refcount4.py
a memory 0xe496a9bc5160
2
b memory 0xe496a9bc5160
3
3
2
freeing 0xe496a9bc5160
$ ./cpython3.14.5/python refcount4.py
a memory 0xfdfcf15b1160
2
b memory 0xfdfcf15b1160
3
3
2
freeing 0xfdfcf15b1160

All of this goes out the window when you’ve got circular references.

Reference cycles

Reference counting is a local algorithm, with no knowledge of other objects. So while automatic reference counting can usually decrement reference counts down to zero (allowing an object to be de-allocated) as scopes complete or as we call del in Python, automatic reference counting cannot decrement reference counts down to zero when cycles are involved.

We’ll observe this by finding our object still in sys.getobjects(limit), which is a list of all allocated objects (only available in these --with-trace-refs builds). It will be in this list even after we call del on our object, because our object contains a circular reference such that the reference count cannot go down to zero on its own.

import sys
class Obj: pass
a = Obj() # 1 reference to Obj()
i = id(a)
a.me = a # 2 references to Obj()
assert any(id(o) == i for o in sys.getobjects(0))
del a # 1 reference to Obj()
assert any(id(o) == i for o in sys.getobjects(0))

refcount5.py

Run it.

$ ./cpython3.14.4/python refcount5.py
$ ./cpython3.14.5/python refcount5.py

It’s easy to confuse del as a method to deallocate an object (in which case: who cares if it references itself, we should be able to delete it, right?). But all del does is remove the name a from scope and decrement the reference count of the object it points to. The object still exists and it still has a reference.

© 2026 Now Let Us. All rights reserved.

Source: Hacker News

Advertisement
Ad slot ready: 5887729102

More in this category

NOW LET US Related – GLM 5.2 Is Out

dev-tools

GLM 5.2 Is Out

Zhipu AI has officially released GLM-5.2, its most powerful open-source model to date, featuring a 1M context window and advanced long-horizon task capabilities. The release underscores Zhipu's commitment to open-source AI and global scientific collaboration amid rising technological restrictions.

NOW LET US Related – Noise infusion banned from statistical products published by Census Bureau

dev-tools

Noise infusion banned from statistical products published by Census Bureau

The U.S. Department of Commerce has banned "noise infusion" from statistical products published by the Census Bureau, a decision that could have severe consequences for both data utility and privacy protection.

NOW LET US Related – Treating pancreatic tumours may have revealed cancer's master switch

dev-tools

Treating pancreatic tumours may have revealed cancer's master switch

A promising new drug called daraxonrasib has shown breakthrough results in treating pancreatic cancer, doubling median survival times. This achievement could pave the way for an entirely new class of cancer treatments.

NOW LET US Related – Every Frame Perfect

dev-tools

Every Frame Perfect

In UI design, perfection isn't just about the start and end states, but every single transition frame in between. Polishing these micro-interactions is key to building user trust.

NOW LET US Related – Leaving Mozilla

dev-tools

Leaving Mozilla

A poignant and candid reflection from a 15-year Mozilla veteran upon their departure. The author highlights the leadership's missteps in trying to emulate tech giants and urges Mozilla to return to its core values: community and uniqueness.

NOW LET US Related – Shepherd's Dog: A Game by the Most Dangerous AI Model

dev-tools

Shepherd's Dog: A Game by the Most Dangerous AI Model

A developer tested Anthropic's latest, supposedly 'too dangerous' AI model by asking it to build a long-held game idea in a single shot. The model succeeded, generating a complete 2,319-line game after a 45-minute reasoning session.

EXPLORE TOPICS

Discover All Categories

Deep dive into the specific technology sectors that matter most to you.