Reference Counting and Garbage Collection
15 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Python frees memory for you, but you still decide how long objects stay alive. This lesson shows the rule behind it and the two cases, cycles and threads, that interviewers like to ask about.
Every object counts its references
A reference count is the number CPython stores in each object: how many names, containers or other objects refer to it. When the count drops to zero the object is destroyed immediately.
CPython keeps a counter inside every object: how many references point to it. Binding a name or adding it to a list adds one. Removing a reference subtracts one. At zero the memory is freed at once.
import sys
class Order: pass
order = Order()before = sys.getrefcount(order)backup = orderholder = [order]print(sys.getrefcount(order) - before)del backup, holderprint(sys.getrefcount(order) - before)Example 1 · An object is freed the moment its last reference goes
class Ticket: def __init__(self, code): self.code = code
def __del__(self): print("freed", self.code)
a = Ticket("T1")b = adel aprint("a deleted")del bprint("b deleted")a deleted freed T1 b deleted
What this code does
del aonly removed one name, so the ticket stayed alive throughb.del bremoved the last reference and__del__ran at once.- This immediate release is a CPython detail. Other implementations may free later.
sys.getrefcount always reads one higher than you expect, because its own argument is a reference. Compare two readings, as here, instead of trusting one number.
What counts as a reference
A weak reference points to an object without raising its reference count. Calling it returns the object while it is alive and None after it has been freed.
Names, list items, dictionary values, attributes, function arguments and closures are all references. An object stays alive while any one of them remains. A weak reference watches an object without keeping it alive.
import weakref
class Report: pass
r = Report()probe = weakref.ref(r)print(probe() is r)r = Noneprint(probe())Example 2 · A container keeps its items alive
import weakref
class Session: pass
s = Session()probe = weakref.ref(s)active = [s]del sprint(probe() is not None)active.clear()print(probe() is None)True True
What this code does
- After
del sthe list still refers to the session. - Clearing the list removes the last reference.
- A forgotten list or cache is the usual cause of a memory leak in Python.
Rebinding r removed the only strong reference, so the object was freed and the weak reference now returns None. Note that del name removes a name, not the object.
Cycles and the garbage collector
The garbage collector is the part of Python that finds groups of objects referring only to each other and frees them. It runs on top of reference counting, which handles every other case.
Two objects that point to each other keep each other's count above zero, even when nothing else can reach them. The garbage collector in the gc module finds such cycles and frees them.
import gc, weakref
class Node: def __init__(self): self.other = None
gc.disable()a, b = Node(), Node()a.other, b.other = b, aprobe = weakref.ref(a)del a, bprint(probe() is not None)gc.collect()print(probe() is None)gc.enable()Example 3 · The collector reports what it freed
import gc
class Node: def __init__(self): self.other = None
gc.disable()for _ in range(3): a, b = Node(), Node() a.other, b.other = b, adel a, bprint("cycles found:", gc.collect() > 0)print("generations:", len(gc.get_threshold()))gc.enable()cycles found: True generations: 3
What this code does
gc.collect()returns how many objects it freed. The exact number varies, so the example prints a comparison.gc.get_threshold()holds one limit per generation.
Counting alone could not free the pair. gc.collect() found them unreachable. Normally the collector runs by itself, in three generations, so most programs never call it.
Threads and the GIL
The GIL, or global interpreter lock, lets only one thread run Python bytecode at a time in CPython. It protects the interpreter's own data, such as reference counts. It does not make your own multi-step code safe.
CPython has one global lock, the GIL. Only one thread runs Python bytecode at a time. This keeps the reference counts safe without a lock on every object.
Example 4 · The GIL does not make counter += 1 safe
import threading
counter = 0lock = threading.Lock()
def work(): global counter for _ in range(1000): with lock: counter += 1
threads = [threading.Thread(target=work) for _ in range(4)]for t in threads: t.start()for t in threads: t.join()print(counter)4000
What this code does
counter += 1is several bytecode steps, and a thread switch can happen between them.- The lock makes the whole update one safe step.
- Python 3.13 also offers an experimental build without the GIL, so locks matter even more there.
Next step
Check your understanding
11 quick questions