languages/reference-counting-how-python-knows-when-to-throw-an-object-away.md

Reference counting: how Python knows when to throw an object away

Programming languages · 5 min read ·

You never tell Python to free memory, yet unused objects disappear. How does it know when one is finished with? CPython keeps a tally on every object: the reference count, the number of names, list slots and attributes pointing at it.

Move labels between boxes in the live demo and watch the counts rise and fall. A box is freed the instant its count reaches zero. Then see the catch: two objects holding on to each other never reach zero, and Python's garbage collector has to rescue them. Every code sample was run on Python 3.13.

Three labelled names a, b and c pointing at one box with a reference count of 3, next to a dashed box showing 0 and freed

The question: who throws things away?

Every time a Python program creates something (a list, a string, an object made from your own class), the computer has to find room for it in memory. Memory is finite, so something has to give that room back when the thing is no longer needed. You never write that step in Python. So who does it, and how does it know when an object is really finished with?

The most common Python (called CPython, the one you get from python.org) uses a simple idea: every object keeps a tally of how many things currently refer to it. That tally is the reference count. When it drops to zero, nobody can reach the object any more, so Python frees it on the spot.

Names are labels, objects are things

A quick reminder of how Python variables work. In a = Box("box") the object Box("box") is created in memory, and a is a name (a label) attached to it. Writing b = a does not copy anything: it attaches a second label to the same object. Every label, every list slot and every attribute that points at the object adds one to its count.

You can peek at the count with sys.getrefcount. It always reports one more than you expect, because passing the object into the function is itself a temporary reference, so the code below subtracts 1.

import sys

class Box:
    def __init__(self, name): self.name = name
    def __del__(self): print(f"{self.name} freed")

a = Box("box")
print(sys.getrefcount(a) - 1)   # 1  (just the name a)
b = a
print(sys.getrefcount(a) - 1)   # 2  (a and b)
c = [a]
print(sys.getrefcount(a) - 1)   # 3  (a, b and the list slot)
del b
print(sys.getrefcount(a) - 1)   # 2
c.clear()
print(sys.getrefcount(a) - 1)   # 1
del a                           # prints: box freed

That is real output from Python 3.13. Notice that del b does not delete the object. It removes the label, and the count goes down by one. The object only disappears when the last label goes. (The method __del__ runs just before an object is freed, which is why we can see the moment it happens.)

Try it: move the labels around

When the count drops to zero

This is why Python code rarely worries about memory. Objects made inside a function are usually referred to only by that function's local names. When the function returns, those names vanish, the counts hit zero and the objects are freed without you doing anything:

def work():
    temp = Box("temp")
    print("inside, refs:", sys.getrefcount(temp) - 1)   # 1
    print("leaving work()")

work()
print("back outside")

The real output is: inside, refs: 1, then leaving work(), then temp freed, then back outside. The object was freed at the exact moment the function ended, before the next line ran.

The catch: objects that point at each other

Reference counting has one famous weakness. Imagine two objects that each hold a reference to the other, like two people each holding the other's hand. Even after you delete every name you own, each still has a count of 1 because the other one is pointing at it. Nobody from outside can reach either object, yet neither count reaches zero. Tick the checkbox in the demo above, point all four names somewhere else, and you will see both boxes stuck at 1 and turn orange.

Python's answer is a second mechanism called the garbage collector (the gc module). From time to time it looks for groups of objects that can't be reached from any name and cleans them up, even if they still hold each other. You can also trigger it yourself, which is what the gc.collect() button does:

import gc, weakref

class Node:
    def __init__(self, name): self.name = name; self.other = None
    def __del__(self): print(f"{self.name} freed")

gc.disable()                      # switch the automatic runs off for this demo
x = Node("x"); y = Node("y")
x.other = y; y.other = x          # a cycle
probe = weakref.ref(x)            # a reference that does not add to the count
del x, y
print("alive after del?", probe() is not None)   # True
gc.collect()                      # prints: x freed, then y freed
print("alive after gc?", probe() is not None)    # False

A weakref is a way of watching an object without keeping it alive, which is why it makes a handy probe here. Outside a demo like this you almost never switch the collector off: it runs by itself, so cycles don't pile up forever.

So what for your own code?

Three takeaways. First, del name removes a label, not the object. If something else still refers to it, such as a list, a global or another object's attribute, it stays in memory. That is the usual reason a Python program uses more memory than you expect: a forgotten list or dictionary is still holding things.

Second, you can usually rely on prompt clean-up in CPython, but don't build logic that depends on the exact moment __del__ runs. This reference-counting behaviour belongs to CPython. Other Python implementations such as PyPy manage memory differently, so freeing can happen later there. For things that must be closed on time, like files, use a with block instead.

Third, cycles aren't a disaster. Linked structures such as trees whose children point back at their parent are common, and the collector handles them. If you are curious how another language answers the same question, compare it with the Rust ownership article on this site, where the compiler decides at build time when to free something.

$ ls languages/

see all →
languages/generators-how-a-python-function-can-pause-in-the-middle-and-pick-up-w.md Generators: how a Python function can pause in the middle and pick up where it left off A generator is a Python function that can pause halfway, hand you a value, and carry on later from the same line with its variables intact… 4 min read · 25 Oct languages/javascript-s-event-loop-why-settimeout-0-runs-after-the-code-below-it.md JavaScript's event loop: why setTimeout(…, 0) runs after the code below it Why does `setTimeout(fn, 0)` run after the lines below it, and why does a promise callback beat it? JavaScript has one worker and two… 5 min read · 22 Oct languages/rust-ownership-why-let-b-a-makes-a-unusable.md Rust ownership: why let b = a makes a unusable Why does Rust refuse to compile `let b = a;` followed by a use of `a`? Because of ownership: every value has exactly one owner, and… 5 min read · 19 Oct languages/ruby-strings-why-changing-b-also-changes-a.md Ruby strings: why changing b also changes a In Ruby, `b = a` does not copy a string: it puts a second label on the same object. So `b << "s"` changes `a` too, while `b += "s"` does… 4 min read · 16 Oct languages/python-default-arguments-why-a-function-remembers-the-last-call.md Python default arguments: why a function remembers the last call Call a small Python function twice and the second call remembers the first. The cause is a default value like `basket=[]`, which Python… 4 min read · 13 Oct languages/go-slices-why-appending-to-a-copy-can-change-the-original.md Go slices: why appending to a copy can change the original Change a copy of a Go slice and sometimes the original changes too. The reason is that a slice is only a small window onto an array, with a… 4 min read · 10 Oct