Command Palette

Search for a command to run...

Hectal
PHASE 13Advanced ~30 min· topic 2 of 9

Topic 13.2

Memory: Reference Counting and Garbage Collection

In one line

CPython frees most objects the instant their reference count hits zero, and a separate cycle collector (the gc module) cleans up objects that keep each other alive. weakref lets you refer to an object without keeping it alive, and del removes a name, not the object.

Think of it like this

A library book with sign-out cards. Each person holding the book has put their name on a card. The librarian does not track the book itself; she just counts the cards. When the last card is torn up, the book goes back on the shelf for someone else. Python keeps a number on every object, the reference count: how many names, containers and other things currently point at it. At zero, the memory is freed at once.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Reference count
A number stored on each object: how many things point at it. At zero the object is freed.
Garbage collection
Automatically freeing memory that the program can no longer reach.
Reference cycle
Objects that point at each other in a loop, so their counts never reach zero by themselves.
Cycle collector
The part of CPython (gc module) that finds and frees unreachable cycles.
Weak reference
A pointer that does not keep its target alive. It gives None after the target is freed.
`__del__`
A method called just before an object is freed. Rarely needed, easy to get wrong.
Memory leak
Memory that is never freed because something still references it.
Immortal object
In 3.12 and later, objects such as None and small ints have a fixed count that never changes.

Step by step

01Counting references

We do not trust the absolute number from sys.getrefcount, because the call itself adds one, and because some objects (None, small ints, interned strings) are shared widely or, since 3.12, immortal. Instead we read the number once as a baseline and print the difference.

Each new name, each place in a list, adds one. Each del takes one away.

main.pywhole filepython
import sys

class Thing:
    pass

x = Thing()
base = sys.getrefcount(x)
y = x
print(sys.getrefcount(x) - base)      # 1
box = [x, x]
print(sys.getrefcount(x) - base)      # 3
del box, y
print(sys.getrefcount(x) - base)      # 0

02Freed the moment the count hits zero

In the first run example the Noisy class prints in __del__. Rebinding n to a new object drops the old one's count to zero, and 'freed first' appears immediately, before the next line of the program. That timing is CPython's reference counting at work.

03A cycle keeps itself alive

Here a.other = b and b.other = a. After del a, b no name can reach either object, but each still has one reference (from the other). Counting alone can never free them. The cycle collector is the safety net.

A cycle keeps itself alivediagram
Rendering diagram…

04Running the collector

gc.collect() runs a full collection and frees unreachable cycles. Normally the collector runs on its own after enough container objects have been created. We call gc.disable() in the demo only so the output is the same every run.

Since Python 3.4 (PEP 442) objects with __del__ in a cycle can be collected. Before that they leaked.

main.pywhole filepython
import gc

gc.disable()
a, b = Node("a"), Node("b")
a.other, b.other = b, a
del a, b
print("nothing freed yet")
gc.collect()          # now they are freed
gc.enable()

05Weak references

weakref.ref(obj) returns a small callable. Calling it gives the object while it is alive and None afterwards. The weak reference does not add to the count, so deleting the last strong name frees the object at once.

Typical uses: a child pointing at its parent (the parent owns the child, not the other way round), caches, and listener lists.

main.pywhole filepython
import weakref

c = Node("c")
ref = weakref.ref(c)
print(ref() is c)
del c
print(ref())          # None

06WeakValueDictionary as a self-cleaning cache

A normal dict cache keeps its values alive forever, which is a classic leak. A weakref.WeakValueDictionary holds values weakly: when nothing else references a value, its entry disappears. It is the right tool for 'cache while someone is using it'.

07Finding who keeps an object alive

When memory stays high, ask who still points at the object. gc.get_referrers(obj) lists the containers that reference it, and tracemalloc (Topic 13.7) shows where the memory was allocated. The usual culprits are module-level lists and dicts, caches without a limit, and bound methods stored as callbacks.

Try it yourself

  1. 1

    Count references yourself

    In the first example, add z = {"k": x} after y = x and print the difference again. A dict value counts as a reference too. Then del z and check it drops.

  2. 2

    Remove the collector call

    In the second example, delete gc.collect() and add import time; time.sleep(0) at the end. The freed a and freed b lines may move or not appear before the program ends: the objects are freed whenever the collector decides to run. This is why you never depend on __del__ for cleanup in a cycle.

Code & diagrams

Reference counts and immediate cleanup New tab

We print differences, never raw counts, because raw numbers depend on the Python version.

Sign in to run this example in your browser.

Expected output

after y = x:        + 1
after a list holds it twice: + 3
after del box:      + 1
after del y:        + 0
seen from inside a function: + 1
rebinding n ...
freed first
deleting n ...
freed second
done
Cycles, the collector and weak references New tab

Remove `gc.collect()` and the two Nodes are still freed, but later and at a time Python picks.

Sign in to run this example in your browser.

Expected output

names deleted, nothing freed yet
freed a
freed b
after gc.collect()
alive: True
freed c
alive after del: None
cached keys: ['d']
freed d
cached keys after del: []
How the collector sees the generationspython

Run on a local Python 3.12; the browser's Python 3.13 prints `(2000, 10, 10)`. The default numbers and the collector's internals have changed between versions, so read the docs for your version before tuning anything.

import gc
print(gc.get_threshold())   # (allocations before a gen-0 run, gen-1 runs, gen-2 runs)

Output

(700, 10, 10)
Caches and callbacks that keep objects alive New tab

The usual real-world leaks: a dict cache, and a bound method stored as a callback. `WeakValueDictionary` and `WeakMethod` are the fixes.

Sign in to run this example in your browser.

Expected output

dict cache still holds it: True
in use, cached: True
not in use, cache entry gone: True True
a stored bound method keeps the object alive: True
freed after the callback list is cleared: True
WeakMethod resolves while alive: True
WeakMethod after delete: None

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Make a weak reference to a list

Try to create a weak reference to a plain list.

main.pywhole filepython
import weakref

numbers = [1, 2, 3]
ref = weakref.ref(numbers)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 4, in <module>
ref = weakref.ref(numbers)
TypeError: cannot create weak reference to 'list' object

Myth vs fact

Myth

del x frees the object.

Fact

del x removes the name x and lowers the count by one. The object is freed only if nothing else points at it.

Myth

Python has no memory leaks because it has a garbage collector.

Fact

A leak is memory you still reference. Unbounded caches, global lists and stored callbacks leak in Python exactly as in any language.

Myth

The garbage collector is what frees most objects.

Fact

In CPython reference counting frees almost everything immediately. The cycle collector only handles the rare objects that sit in cycles.

Myth

__del__ is a reliable destructor, like in C++.

Fact

It runs when the object is freed, which can be late (cycles) or never (at interpreter shutdown). Use with and context managers for cleanup.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The cycle collector is generational: new objects go into generation 0, and survivors move to older generations that are collected less often. The idea is that most objects die young, so checking the young ones often is cheap and finds most garbage. The details have changed between versions, so measure with the version you run.

  • ▸

    Immortal objects (PEP 683, 3.12) have a fixed reference count so that the interpreter, and sharing between subinterpreters and the free-threaded build, need no count updates for them. It is also why sys.getrefcount(None) gives a huge constant number.

  • ▸

    Long-running services: call gc.freeze() after start-up in a pre-forking server (gunicorn with preloaded code, for example). It moves every object alive at that moment into a permanent set that the collector never scans, so the collector does not touch (write to) those objects' headers and the memory pages stay shared between parent and workers. (Copy-on-write means a forked child shares its parent's memory until one of them changes a page.) It does not stop reference counts changing: counts live inside the objects, so even *reading* an object can write to its page. That is why memory sharing across forked workers is never perfect in CPython.

  • ▸

    What triggers the collector (in the classic generational design, 3.12 and 3.13): it counts container-object allocations minus deallocations, and when the count passes the generation-0 threshold, it collects the young generation. Objects that survive move to generation 1, then 2. A full collection of the oldest generation is also skipped unless the number of long-lived objects has grown by about a quarter since the last one, which stops huge heaps being rescanned constantly. Only objects that can hold references (containers) are tracked: an int or a str is not, so a workload that only creates numbers and strings adds no collector work.

  • ▸

    gc.callbacks, gc.get_stats() and gc.set_debug(gc.DEBUG_STATS) show what the collector is doing in a live process, and gc.collect() returns the number of unreachable objects it found. If you do disable the collector for speed (some batch jobs do), take over the job: call gc.collect() at known points, because a process that creates cycles with the collector off will grow without bound.

  • ▸

    For ML work, large arrays live in native memory and are freed when their Python wrapper's count reaches zero. Holding a reference to a big tensor or DataFrame in a notebook cell output, a logger or an exception traceback is the usual way a pipeline runs out of memory.

Remember this

  1. 1

    Things that add one to the count: a name bound to the object, an item in a list, tuple, set or dict, an attribute of another object, an argument while a function runs, a closure. Things that subtract one: del name, rebinding the name to something else, a name going out of scope when a function returns, removing the item from its container. sys.getrefcount(x) shows the number, but it is always one higher than you expect because passing x to the function adds a temporary reference.

  2. 2

    Because freeing is immediate, Python has deterministic cleanup: when the last reference to a file or lock object vanishes, its __del__ runs right then. This is a CPython property, not a language promise. PyPy and others free objects later. For resources like files and locks, always use with and context managers (Topics 7.4 and 9.8) instead of relying on counts.

  3. 3

    Reference counting has one weakness: cycles. If object A points to B and B points to A, each keeps the other's count at 1 forever, even when nothing else can reach them. To fix this, CPython has a cycle collector (the gc module). It periodically looks at container objects (lists, dicts, instances) and finds groups that are only reachable from each other, then frees them. You can run it yourself with gc.collect() and pause it with gc.disable().

  4. 4

    del x does not delete an object. It removes the name x and lowers the count by one. The object is freed only if that was the last reference. Beginners often write del big_list hoping to 'free memory' while another name or a cache still holds it. The memory then stays.

  5. 5

    weakref solves two problems. A weak reference points at an object without raising its count, and returns None once the object is gone. That lets caches, observer lists and parent pointers avoid keeping objects alive. weakref.WeakValueDictionary is a cache that drops entries automatically. Not everything can be weakly referenced: plain list, dict, int, str and tuple objects cannot (a subclass such as class Bag(list): pass can), but ordinary class instances, functions and sets can.

  6. 6

    Memory 'leaks' in a garbage-collected language are almost always objects you still reference: a growing global list, a cache with no limit, a default argument, a closure holding a big object, or an exception traceback kept alive. Python gives no free. The fix is to find who still points at the object (gc.get_referrers, tracemalloc in Topic 13.7) and drop or weaken that reference.

Explain it without notes

01

How does CPython decide when to free an object? What are the cases that reference counting cannot handle?

02

What does del x do?

03

What is a weak reference and when would you use one?

04

How would you look for a memory leak in a long-running Python service?

Practice

01

Create an instance, store it twice in a dict, and print how many extra references that adds. Delete the dict and print again.

02

Make a weak reference to an instance, delete the instance, and show that the reference now returns None.

03

Create two Node objects that point at each other, delete both names and show that they are freed only after gc.collect() (disable the automatic collector first).

Trade-offs

  • ↔

    Reference counting gives prompt, predictable cleanup but costs time. Every assignment updates a count, which is one reason CPython is slower than a tracing collector for some workloads. The cycle collector adds pauses. (A tracing collector, used by Java and Go, skips the counts and instead walks from the live objects to find what is still reachable.)

  • ↔

    Weak references avoid leaks but make objects optional. Code that uses one must handle None (the target may be gone), so use them only where 'it may disappear' is a valid state.

  • ↔

    **__del__ versus context managers.** Finalisers run at an unpredictable time in cycles and at exit. with blocks give a guaranteed time. Prefer with.

Done when you can

  • Done when you can explain how reference counts rise and fall for a small program.

  • Done when you can say why a reference cycle is not freed by counting alone and what frees it.

  • Done when you can explain what del really does.

  • Done when you can use weakref.ref and WeakValueDictionary correctly.

  • Done when you can name three ways a Python program leaks memory and how you would find them.