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 (
gcmodule) that finds and frees unreachable cycles. - Weak reference
- A pointer that does not keep its target alive. It gives
Noneafter 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
Noneand 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.
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) # 002Freed 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.
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.
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.
import weakref
c = Node("c")
ref = weakref.ref(c)
print(ref() is c)
del c
print(ref()) # None06WeakValueDictionary 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
Count references yourself
In the first example, add
z = {"k": x}aftery = xand print the difference again. A dict value counts as a reference too. Thendel zand check it drops. - 2
Remove the collector call
In the second example, delete
gc.collect()and addimport time; time.sleep(0)at the end. Thefreed aandfreed blines 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
We print differences, never raw counts, because raw numbers depend on the Python version.
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
doneRemove `gc.collect()` and the two Nodes are still freed, but later and at a time Python picks.
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: []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)The usual real-world leaks: a dict cache, and a bound method stored as a callback. `WeakValueDictionary` and `WeakMethod` are the fixes.
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: NoneBreak 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.
import weakref
numbers = [1, 2, 3]
ref = weakref.ref(numbers)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
intor astris not, so a workload that only creates numbers and strings adds no collector work. - ▸
gc.callbacks,gc.get_stats()andgc.set_debug(gc.DEBUG_STATS)show what the collector is doing in a live process, andgc.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: callgc.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
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 passingxto the function adds a temporary reference. - 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 usewithand context managers (Topics 7.4 and 9.8) instead of relying on counts. - 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
gcmodule). 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 withgc.collect()and pause it withgc.disable(). - 4
del xdoes not delete an object. It removes the namexand lowers the count by one. The object is freed only if that was the last reference. Beginners often writedel big_listhoping to 'free memory' while another name or a cache still holds it. The memory then stays. - 5
weakrefsolves two problems. A weak reference points at an object without raising its count, and returnsNoneonce the object is gone. That lets caches, observer lists and parent pointers avoid keeping objects alive.weakref.WeakValueDictionaryis a cache that drops entries automatically. Not everything can be weakly referenced: plainlist,dict,int,strandtupleobjects cannot (a subclass such asclass Bag(list): passcan), but ordinary class instances, functions and sets can. - 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,tracemallocin Topic 13.7) and drop or weaken that reference.
Explain it without notes
How does CPython decide when to free an object? What are the cases that reference counting cannot handle?
What does del x do?
What is a weak reference and when would you use one?
How would you look for a memory leak in a long-running Python service?
Practice
Create an instance, store it twice in a dict, and print how many extra references that adds. Delete the dict and print again.
Make a weak reference to an instance, delete the instance, and show that the reference now returns None.
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.withblocks give a guaranteed time. Preferwith.
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
delreally does.Done when you can use
weakref.refandWeakValueDictionarycorrectly.Done when you can name three ways a Python program leaks memory and how you would find them.