PYTHON / VARIABLES AND DATA TYPES
Memory, reference counting, and garbage collection
Track how CPython decides an object is dead: reference counts drop to zero immediately, and the cycle collector cleans up what counting cannot.
What you will learn
- Read sys.getrefcount() correctly, allowing for its own temporary argument reference
- Predict exactly when an object is freed as names are rebound or deleted
- Recognise a reference cycle and confirm with weakref that gc.collect() clears it
- Explain why del removes a name but does not necessarily free memory
Understanding Memory, reference counting, and garbage collection
Every CPython object carries a small integer counter alongside its value: the number of references pointing at it. Binding a name, appending the object to a list, passing it as an argument or storing it as an attribute each increments that counter; rebinding the name, deleting it, or destroying the container decrements it. The moment the counter reaches zero the object is destroyed on the spot, in the middle of whatever statement caused the last decrement. That is why memory in Python usually feels reclaimed instantly, with no pause and no separate cleanup phase.
Reference counting has one hole it cannot plug. If object x holds a reference to y and y holds a reference back to x, then even after every name outside the pair disappears, each still has a count of one, kept alive purely by the other. Neither will ever reach zero, so CPython runs a second mechanism, the cyclic garbage collector in the gc module, which periodically walks tracked container objects, finds groups that are unreachable from the running program, and frees the whole group. Only containers are tracked; an int or a str cannot refer to anything, so it can never be part of a cycle and is handled by counting alone.
The mental model to keep is that objects are not owned by names. A name is one reference among possibly many, so del data does not free anything if a list, a dict, a closure or another name still points at the same object. When a program's memory grows without bound, the cause is almost always a reference you forgot about (a cache, a module-level list, an exception traceback holding a frame) rather than a collector that failed to run. Calling gc.collect() cannot help in that case, because from the interpreter's point of view the object is still in use.
import sys
class Noisy:
def __init__(self, tag):
self.tag = tag
def __del__(self):
print("freed", self.tag)
a = Noisy("A")
print("references:", sys.getrefcount(a) - 1)
b = a
print("references:", sys.getrefcount(a) - 1)
del b
print("references:", sys.getrefcount(a) - 1)
del a
print("interpreter still running")An object lives exactly as long as something refers to it: reference counting frees it the instant the count hits zero, and the cycle collector handles the references that keep each other alive.
Worked examples
A cycle that outlives its function
Two objects pointing at each other survive the return of the function that made them, until the cyclic collector runs.
import gc
import weakref
class Node:
def __init__(self, name):
self.name = name
self.peer = None
def make_cycle():
x = Node("x")
y = Node("y")
x.peer = y
y.peer = x
return weakref.ref(x)
gc.disable()
ref = make_cycle()
print("alive after return:", ref() is not None)
gc.collect()
print("alive after gc.collect():", ref() is not None)
gc.enable()Example explained
Line 1weakref.ref(x) observes the object without adding to its reference count, so it is a safe liveness probe.
Line 2When make_cycle returns, the local names x and y vanish but each Node is still referenced by the other's peer attribute, so both counts stay at one.
Line 3gc.disable() stops an automatic collection from happening between the two checks, making the timing deterministic.
Line 4gc.collect() sees the pair is unreachable from any live name and frees it, so calling the weak reference now yields None.
del removes a name, not an object
Deleting one of two names leaves the object fully usable through the other name.
import sys
data = [1, 2, 3]
alias = data
print("references:", sys.getrefcount(data) - 1)
del data
alias.append(4)
print("alias still works:", alias)
try:
print(data)
except NameError as e:
print("data is gone:", e)Example explained
Line 1data and alias are two references to one list, so the adjusted count is 2.
Line 2del data decrements the count to 1; the list itself is untouched and keeps its contents.
Line 3The append proves the object survived, and the NameError proves only the binding was removed.
Important notes
Reference counting is a CPython implementation detail; PyPy and Jython free objects at unpredictable times, so code that depends on immediate destruction is not portable.
Small integers, short strings and None are cached or immortal and shared across your whole program, so their reference counts are huge and meaningless to inspect.
Common mistakes
Reading sys.getrefcount() as an absolute truth: passing the object as an argument creates one extra temporary reference, so a freshly bound name reports 2 and beginners conclude a phantom copy exists.
Assuming del x or x = None frees memory: if a list, dict or other name still references the object, the count never reaches zero and nothing is released.
Relying on __del__ for important cleanup such as closing files: it runs whenever the last reference disappears, which may be inside a cycle collected much later, so use a with block instead.
Try it yourself
Change, predict, then run
Write a class Box whose __del__ prints its label, create three boxes inside a list, then pop one element and delete the list, printing a marker line between each action. Note from the output exactly which statement destroyed which box.
Open the Python workspaceCheck your understanding
A function creates two objects that store references to each other, then returns None. Immediately after the call, what is true of those two objects?
- They are still in memory, because each keeps the other's reference count above zero
- They were freed the moment the function returned, since their local names disappeared
- They were never really created, because Python detects the cycle at definition time
- They can only be freed if you call del on them from outside the function
Show answer
The local names vanish, but each object is still referenced by the other's attribute, so neither count reaches zero and reference counting cannot free them; the cyclic collector does that later. The second option describes what would happen for ordinary non-cyclic locals, which is why it is tempting, but a mutual reference keeps both counts at one.