Garbage collection: why an object is alive when nothing refers to it
In the standard CPython build an object dies the moment its last reference disappears — no collector is needed for that. The collector is needed for cycles, and only for them. And an object that “will not die” may be held not by a garbage cycle but by a reference from a live object that you cannot see in the code: an exception's traceback, a cache key, a method in a subscriber list.
Full technical treatment
TL;DR
In the standard CPython build an object dies when its last reference
disappears — at once, on that very line. That is the job of reference counting, not of the garbage
collector. del does not destroy an object; it removes a name and decrements
the count by one. The collector (gc) is needed for exactly one case the count
cannot handle: a cycle. Two nodes refer to each other, the names are gone,
yet each count stays at one — and the objects live until gc.collect(), which
finds a group with no outside references and frees it.
Beyond that is what separates knowing from having read. When memory “is
not released”, look not only for a garbage cycle but also for a reference from
a live object that you cannot see in the code. An exception stored in an attribute holds its
traceback, the traceback holds the failed function's frame, and the frame
holds all its local variables. lru_cache on a method holds self in its key.
A method put in a subscriber list holds its object in __self__. All of it is
reachable from a live object, and the collector will not touch it —
gc.collect() changed nothing in the run. But one change breaks the path, and
the object dies on that same line. And a full collection is paid for not by
garbage but by live objects: a million live lists — 39.88 ms per
gc.collect() without a single garbage object (3.13.7).
Since 3.13 CPython has two builds, and everything above is about the standard
one, with the GIL. In the build without the GIL (3.13t, 3.14t) an instance
created and released by one thread also dies on the del line. But a
module-level function waits for the collector after del, an object whose last
reference was released by another thread outlives that line, and gc.freeze()
does not take everything off the collection.
- that a variable is a name bound to an object;
- that an object can be put in a list, in a dict, in another object's attribute;
- how
try/exceptworks and what an instance method is.
- reference counting,
gc.collect(), unreachable cycles,weakref; - the traceback and frame in an exception,
lru_cacheon a method,WeakMethod, PEP 442,gc.freeze().
Base: an object dies when its last reference disappears
Every object in CPython has a counter: how many references to it exist right now. A name is a reference. A list element is a reference. Another object's attribute is a reference. When a reference appears the count goes up; when it disappears the count goes down. Once it reaches zero, the object is freed at once.
That is how the standard CPython build works — the one installed by default. Since 3.13 there is a second one, without the GIL; there the rule has exceptions, covered at the end of the lesson. They do not affect the Junior-level answer: for an object that one thread creates and releases, the rule holds in both builds.
Hence the Junior-level answer to “how is memory freed in Python”: the main mechanism is reference counting: an object is freed as soon as no references to it remain. The garbage collector supplements it and is needed for cycles.
And a consequence people trip over: del x does not delete the object. It
deletes the name x and decrements the count by one. If that was the last
reference, the object dies. If not, it lives on.
You can check whether an object is alive without holding it — with a weak
reference from the weakref module. It does not increase the count, and after
the object's death it returns None. Every run in this lesson uses that trick.
Mechanism 1: a cycle is the only thing the count cannot handle
Without a cycle, everything happens on the del line:
1) no cycle
before del: alive
right after del: dead — freed by reference counting, no collector needed
Now two nodes that refer to each other:
a, b = Node("a"), Node("b")
a.other, b.other = b, a
del a, b2) a two-node cycle
sys.getrefcount(a) before del: 3
after del a, b: alive and alive
gc.collect() returned 2; now: dead and dead
sys.getrefcount shows 3: the name a, the reference from b.other and the
function's own argument — the call itself adds the third unit.
After del there are no names, but each node still has one reference — from
its neighbour. The count will never drop to zero: the program cannot reach the
nodes, and they hold each other.
That kind of group is what the cycle collector looks for. The documentation
describes its role in one phrase:
the collector supplements the reference counting already used in Python
.
It walks the tracked objects (containers — lists, dicts, class instances;
numbers and strings it does not track, since they never refer to other
objects) and finds groups with no references from outside. gc.collect()
returns the number of unreachable objects found — 2 here. It also runs on its
own, by thresholds on the number of objects created; how those thresholds and
generations work is covered in the article
on memory management.
Mechanism 2: the object is alive not because of a cycle but because of a reference you cannot see
The first thing that comes to mind for “why is memory not released” is “there is a cycle somewhere.” But an object can also be held by an ordinary reference from a live object — quite legitimately, it is just not in plain sight in the code. Here are three such references.
An exception's traceback holds the frame
An exception has a __traceback__ attribute. The traceback holds the frame of
every function the exception passed through, and a frame holds all its local
variables. The language reference says so directly, explaining why the name
from except ... as e disappears after the block:
Exceptions are cleared because with the traceback attached to them, they form a
reference cycle with the stack frame, keeping all locals in that frame alive
until the next garbage collection occurs.
The run checks three cases: the exception is stored nowhere; it is stored in a local variable of the same function; it is stored in an attribute of a live object.
3) an exception and the frame of the failed function
the exception is stored nowhere: payload after the function returns — dead
stored in the local err: payload after return — alive
cycle: frame -> err -> exception -> __traceback__ -> frame
after gc.collect(): dead
stored in svc.last_error: payload after return — alive
path from the live svc: svc -> exception -> __traceback__ -> frame -> payload
gc.collect() with svc alive: alive
svc.last_error = None: dead
the name e after the except block: NameError — the interpreter itself deleted it
The second and third cases look the same — “payload is alive” — but they are
built differently, and the cure depends on it. The local err = e brings back
exactly the cycle from the reference: after the function returns nothing can
reach it, and the object waits for the collector. With the attribute
svc.last_error everything is reachable from the live service — and the
collector will not touch it: gc.collect() with svc alive did not free the
object. But there is no need to wait for it either: svc.last_error = None, and the object died on that very line. Store the error's text in the
attribute, not the exception itself.
lru_cache on a method holds self
4) functools.lru_cache on a method
del rep: alive — self sits in the cache key
gc.collect(): alive
Report.total.cache_clear(): dead
The cache lives on the function, that is on the class, and self is part of
the key. The Python documentation describes this in its FAQ:
The disadvantage is that instances are kept alive until they age out of the cache or until the cache is cleared
.
With maxsize=None there is no eviction at all, and every instance whose
method was called even once lives until cache_clear() or until the process
ends.
A method in a subscriber list holds the object
5) a bound method in a subscriber list
callbacks.append(w.on_event); del w: alive — the method holds __self__
gc.collect(): alive
callbacks.clear(): dead
the same via weakref.WeakMethod; del w: dead
weak_callbacks[0]() -> None
w.on_event is a bound method, an object whose __self__ holds w.
Subscribing a method to an event means holding a strong reference to the whole
object. weakref.WeakMethod refers to it weakly: the object dies, the
subscription returns None, and it can be dropped at the next dispatch.
Mechanism 3: since 3.4, __del__ in a cycle does not block collection
Before Python 3.4, the collector would not touch a cycle containing an object
with __del__: it was unclear in what order to call finalizers when objects refer
to each other. Such objects piled up in gc.garbage. PEP 442 removed that:
to be able to define and run finalizers for any object, regardless of their position in the object graph
.
6) __del__ on both nodes of a cycle
gc.collect() returned 2; finalizers ran: ['a', 'b']
gc.garbage: []
Both finalizers were called, and gc.garbage is empty. But the PEP explicitly
leaves the order in which they are called undefined:
For CI objects, the order in which finalizers are called (step 2 above) is undefined
.
Here it came out a, then b — do not rely on that. And in the __del__ of
an object in a cycle, its neighbour may already be finalized.
Deeper: what a full collection pays for
A full collection — gc.collect() with no arguments — walks all tracked
objects, live ones included: to know that a group is unreachable, you have to
know what everything else refers to. So its time grows with the number of live
containers, even when there is no garbage at all:
PY 3.13.7 | best of 7 calls to gc.collect()
live lists gc.collect() per object
10 000 0.75 ms 45.8 ns
100 000 3.57 ms 32.8 ns
1 000 000 39.88 ms 39.6 ns
empty heap 0.29 ms
A few tens of nanoseconds per live object — from 32.8 to 45.8 ns in this table: a million live lists is almost a 40 ms pause per full collection. The collector itself runs a full collection rarely — the thresholds are designed to keep most of the work in the young generation — but a process holding a large long-lived cache in memory pays for it on every such collection.
Taking live objects out of the walk is what gc.freeze(), added in 3.7, can
do: it moves everything tracked into a permanent generation, and later
collections do not walk it. But it was designed not as a general cure for
pauses but for one case — more on it below.
the same million live lists
before gc.freeze(): 38545.7 µs
after gc.freeze(): 0.3 µs, 1 005 045 objects in the permanent generation
The documentation suggests it for processes that fork without exec: the
collector in the child does not touch frozen objects, and fewer memory pages
are copied on write. As a general trick against pauses it works with two
caveats. Garbage cycles among the frozen objects are no longer collected until
gc.unfreeze() is called — that memory is given away for good. And in the
build without the GIL freezing does not take everything off the collection; the
next section shows it.
Deeper: the build without the GIL — the same rule, with exceptions
Since 3.13 CPython can also be built without the GIL (PEP 703). Reference counting works differently there: an object has an owning thread, and operations from that thread take the fast path while those from another thread take the shared one. How that affects speed is covered in the article on the GIL, and how it affects object size in the article on memory management. Here the question is what it means for “when does the object die”.
The main rule of the lesson holds: an instance created and released by one
thread dies on the del line without the GIL too. All six experiments in
bench/gc/alive.py on 3.13t and 3.14t give the same “alive or dead” answers as with the
GIL. But three things diverge:
2) a function defined at module level
right after del: alive
after gc.collect(): dead
for comparison, a nested function right after del: dead
3) the last reference is deleted by a thread other than the one that created the object
alive in that thread right after del: 19 of 20
alive in the main thread after join(): 0 of 20
4) gc.freeze() and a million live lists, best of 7 gc.collect() calls
before gc.freeze(): 37272.2 µs
after gc.freeze(): 23155.0 µs, 1 007 024 objects in the permanent generation
A module-level function waits for the collector. With the GIL it dies on the
del line, without it only on gc.collect(); a nested function dies at once in
both builds. The same effect shows up in bench/gc/alive.py: in the sixth experiment
gc.collect() without the GIL returned 3, not 2, and the third object was the
class body function — with the GIL the count freed it right after the class was
created.
The last reference from another thread. The main thread created the object,
and another thread took and deleted the last reference. Right after del in
that thread the object was alive in 19 attempts out of 20 on 3.14t and 20 out
of 20 on 3.13t; in the main thread after join() it was dead in all of them.
With the GIL it is dead at once, 0 out of 20 on all four versions. The number of
attempts is printed for a reason: the result depends on thread scheduling.
gc.freeze() does not take everything off. With the GIL, after freezing, a
collection over a million live lists takes a fraction of a microsecond —
0.2–0.3 µs in this same run. Without the GIL it is 23.2 ms out of 37.3 on 3.14t
and 23.4 out of 63.5 on 3.13t: a noticeable part of the collector's work stays.
Hence the precise wording for an interview: in the standard CPython build an object without a cycle dies at once when its count reaches zero; in the build without the GIL that is true for a single thread's object, but not for every object.
How to answer in an interview
Short answer: in CPython memory is freed first of all by reference counting:
an object dies as soon as its last reference disappears, at once. del
deletes a name, not an object. The garbage collector supplements the count and
is needed for cycles: objects that refer to each other would never be freed by
the count, and gc finds such groups with no outside references.
That is enough to answer correctly. Beyond it is what you add if the interviewer digs.
If the interviewer digs deeper
If leaks come up, a good answer starts not with cycles but with the fact that
an object can be held by an ordinary reference from a live object that you
cannot see: an exception in an attribute holds its traceback, the frame and
all its local variables; lru_cache on a method holds self in its key; a
method in a subscriber list holds its object in __self__. All of it is
reachable, and the collector will not touch it. One change cures it: store the
error's text, clear the cache, subscribe through weakref.WeakMethod.
A detail worth adding is why the name from except ... as e disappears after the
block: otherwise the frame would hold the exception, the exception would hold
the frame through its traceback, and there would be a cycle with all the local
variables. That is written in the language reference. And about the cost of
collection: it grows with the number of live objects — a million live
lists, 39.88 ms per gc.collect() with no garbage (3.13.7) — and
gc.freeze() takes them out of the walk, in the standard build.
If the build without the GIL comes up, it is worth saying that “dies on the
same line” is not always true there: a module-level function waits for the
collector, and an object whose last reference was released by another thread
outlives del in that thread.
Next they ask
Can the collector be turned off?
Yes — gc.disable(), and the documentation allows it if the program creates no
cycles. Reference counting keeps working, and objects without cycles are freed
as before. But any cycle — including the “frame — exception” cycle — will live
until an explicit gc.collect() or until the process ends. gc.freeze() is
gentler: it takes what already exists
out of the walk, while new cycles are still collected.
How do you find what is holding an object?
gc.get_referrers(obj) returns the objects that refer to it directly — the
documentation asks that it be used for debugging only. Following a chain of
such calls leads to the holder: an attribute, a cache, a subscriber list. To
check whether the object died after the fix, use a weak reference, as in the
lesson's runs.
Is __del__ in a cycle a leak?
Since 3.4, no: PEP 442 allowed the collector to finalize such cycles, and
gc.garbage is empty. But the order of finalizer calls in a cycle is
undefined, and in __del__ a neighbour in the cycle may already be finalized.
Common misconceptions
del x deletes the object
It deletes the name x and decrements the reference count by one. The object dies only if that was the last reference. In a two-node cycle, after del a, b both are alive and die only on gc.collect().
Memory in Python is freed by the garbage collector
First of all by reference counting, and at once: an object without a cycle is freed on the same line where its last reference disappeared, even with the collector off. The collector supplements the count and is needed only for cycles.
If an object is not freed, there is a cycle somewhere
It can also be an ordinary reference from a live object that you cannot see in the code: an exception in an attribute holds the frame and all its local variables, lru_cache on a method holds self, a bound method in a subscriber list holds its object. All of it is reachable: gc.collect() in the run did not free such an object, and one change frees it on that same line.
Objects with __del__ in a cycle are never collected
That was before 3.4. Since PEP 442 such cycles are collected: in the run gc.collect() returned 2, both finalizers ran, and gc.garbage is empty. Only the order in which the finalizers are called is undefined.
An object with no references dies at once in any CPython build
That is the standard build, with the GIL. In the build without the GIL a module-level function is alive after del and dies only on gc.collect(), and an object whose last reference was deleted by a thread other than the one that created it is alive in that thread right after del — 19 attempts out of 20 on 3.14t. An instance of a single thread dies at once in both builds.
Garbage collection costs more the more garbage there is
A full collection walks all tracked objects, live ones too. Without a single garbage object: 0.75 ms with ten thousand live lists and 39.88 ms with a million (3.13.7). After gc.freeze() the same million are not walked.
Version history
| Version | Change | What it means for code |
|---|---|---|
| 3.4 | PEP 442: cycles with __del__ are collected and no longer end up in gc.garbage. Before that the advice was to keep objects with __del__ out of cycles. | |
| 3.7 | gc.freeze(), gc.unfreeze() and gc.get_freeze_count() appear — a way to take long-lived objects out of the collector's walk, above all before a fork. | |
| 3.13 | The young-generation threshold defaults to 2000 instead of 700. Verified by the run: gc.get_threshold() gives (700, 10, 10) on 3.11.15 and 3.12.3 and (2000, 10, 10) on 3.13.7 and 3.14.7. The change of threshold is visible in the source at the v3.13.0 tag too; what exactly these numbers count is in the memory management article. The build without the GIL also appears (PEP 703; sys.version says experimental free-threading build): an instance of one thread dies on the del line there too, while a module-level function waits for the collector (bench/gc/freethreaded.py). | |
| 3.14 | In the standard build: in 3.14.0 the collector became incremental, and in 3.14.5 that was rolled back — details and the source are in the memory management article. It does not affect the behaviour covered in this lesson: all six experiments in bench/gc/alive.py give the same output on 3.14.7 as on 3.11–3.13. The build without the GIL (sys.version now says free-threading build, without experimental) is built with a different collector — the source path in the library is Python/gc_free_threading.c versus Python/gc.c — and behaves differently: gc.freeze() brings a full collection over a million live lists down from 37.3 to 23.2 ms, not to a fraction of a microsecond. |
Practice
Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a run of the script rather than being assigned.
Practice · predict the output
class Node: pass gc.disable() a = Node() b = Node() a.other = b b.other = a r = weakref.ref(a) del a, b print(r() is None) gc.collect() print(r() is None)
Practice · estimate
Check yourself
An object with no cycles, one reference to it — the name x. The collector is off via gc.disable(). What happens on del x?
How it was measured
The numbers in this lesson come from these scripts. Each one opens right from here — together with the record of its run: what it ran on, what came out, and with what spread.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- In the standard CPython build an object dies when its last reference disappears — at once, on that very line. That is the job of reference counting, not of the garbage collector.
deldoes not destroy an object; it removes a name and decrements the count by one. The collector (gc) is needed for exactly one case the count cannot handle: a cycle. Two nodes refer to each other, the names are gone, yet each count stays at one — and the objects live untilgc.collect(), which finds a group with no outside references and frees it. - Beyond that is what separates knowing from having read. When memory “is not released”, look not only for a garbage cycle but also for a reference from a live object that you cannot see in the code. An exception stored in an attribute holds its traceback, the traceback holds the failed function's frame, and the frame holds all its local variables.
lru_cacheon a method holdsselfin its key. A method put in a subscriber list holds its object in__self__. All of it is reachable from a live object, and the collector will not touch it —gc.collect()changed nothing in the run. But one change breaks the path, and the object dies on that same line. And a full collection is paid for not by garbage but by live objects: a million live lists — 39.88 ms pergc.collect()without a single garbage object (3.13.7). - Since 3.13 CPython has two builds, and everything above is about the standard one, with the GIL. In the build without the GIL (3.13t, 3.14t) an instance created and released by one thread also dies on the
delline. But a module-level function waits for the collector afterdel, an object whose last reference was released by another thread outlives that line, andgc.freeze()does not take everything off the collection.
In fact
- It deletes the name
xand decrements the reference count by one. The object dies only if that was the last reference. In a two-node cycle, afterdel a, bboth are alive and die only ongc.collect(). - First of all by reference counting, and at once: an object without a cycle is freed on the same line where its last reference disappeared, even with the collector off. The collector supplements the count and is needed only for cycles.
- It can also be an ordinary reference from a live object that you cannot see in the code: an exception in an attribute holds the frame and all its local variables,
lru_cacheon a method holdsself, a bound method in a subscriber list holds its object. All of it is reachable:gc.collect()in the run did not free such an object, and one change frees it on that same line. - That was before 3.4. Since PEP 442 such cycles are collected: in the run
gc.collect()returned 2, both finalizers ran, andgc.garbageis empty. Only the order in which the finalizers are called is undefined. - That is the standard build, with the GIL. In the build without the GIL a module-level function is alive after
deland dies only ongc.collect(), and an object whose last reference was deleted by a thread other than the one that created it is alive in that thread right afterdel— 19 attempts out of 20 on 3.14t. An instance of a single thread dies at once in both builds. - A full collection walks all tracked objects, live ones too. Without a single garbage object: 0.75 ms with ten thousand live lists and 39.88 ms with a million (3.13.7). After
gc.freeze()the same million are not walked.
By version
- 3.4
- PEP 442: cycles with
__del__are collected and no longer end up ingc.garbage. Before that the advice was to keep objects with__del__out of cycles.< - 3.7
gc.freeze(),gc.unfreeze()andgc.get_freeze_count()appear — a way to take long-lived objects out of the collector's walk, above all before afork.<- 3.13
- The young-generation threshold defaults to 2000 instead of 700. Verified by the run:
gc.get_threshold()gives(700, 10, 10)on 3.11.15 and 3.12.3 and(2000, 10, 10)on 3.13.7 and 3.14.7. The change of threshold is visible in the source at the v3.13.0 tag too; what exactly these numbers count is in the memory management article. The build without the GIL also appears (PEP 703;sys.versionsaysexperimental free-threading build): an instance of one thread dies on thedelline there too, while a module-level function waits for the collector (bench/gc/freethreaded.py).< - 3.14
- In the standard build: in 3.14.0 the collector became incremental, and in 3.14.5 that was rolled back — details and the source are in the memory management article. It does not affect the behaviour covered in this lesson: all six experiments in
bench/gc/alive.pygive the same output on 3.14.7 as on 3.11–3.13. The build without the GIL (sys.versionnow saysfree-threading build, withoutexperimental) is built with a different collector — the source path in the library isPython/gc_free_threading.cversusPython/gc.c— and behaves differently:gc.freeze()brings a full collection over a million live lists down from 37.3 to 23.2 ms, not to a fraction of a microsecond.<
What is covered
- Base: an object dies when its last reference disappears
- Mechanism 1: a cycle is the only thing the count cannot handle
- Mechanism 2: the object is alive not because of a cycle but because of a reference you cannot see
- Mechanism 3: since 3.4, `__del__` in a cycle does not block collection
- Deeper: what a full collection pays for
- Deeper: the build without the GIL — the same rule, with exceptions
- How to answer in an interview
- Next they ask
- Common misconceptions
- Version history
- Practice
- Check yourself
- How it was measured
Sources & further reading
6 SOURCES
- Language reference — the try statement, the except clauseOfficial documentation. The most direct explanation of how a traceback holds memory: “Exceptions are cleared because with the traceback attached to them, they form a reference cycle with the stack frame, keeping all locals in that frame alive until the next garbage collection occurs.” That is also why the name after except as disappears.https://docs.python.org/3.14/reference/compound_stmts.html#except-clause
- gc — Garbage Collector interfaceOfficial documentation. The collector's role: “the collector supplements the reference counting already used in Python.” gc.collect: “With no arguments, run a full collection.” gc.freeze, added in 3.7: “Freeze all the objects tracked by the garbage collector; move them to a permanent generation and ignore them in all the future collections.”https://docs.python.org/3.14/library/gc.html
- PEP 442 — Safe object finalizationPEP. Antoine Pitrou, Final, Python 3.4. Before it, the collector left an object with __del__ in a cycle alone. The PEP's goal: “to be able to define and run finalizers for any object, regardless of their position in the object graph.” And the limit of the guarantee: “For CI objects, the order in which finalizers are called (step 2 above) is undefined.”https://peps.python.org/pep-0442/
- Programming FAQ — How do I cache method calls?Official documentation. Why lru_cache on a method holds instances: “It creates a reference to the instance unless special efforts are made to pass in weak references” and “The disadvantage is that instances are kept alive until they age out of the cache or until the cache is cleared.”https://docs.python.org/3/faq/programming.html#faq-cache-method-calls
- PEP 703 — Making the Global Interpreter Lock Optional in CPythonPEP. Sam Gross, Final, Python 3.13. The CPython build without the GIL and its reference counting: an object has an owning thread, and operations from that thread take the fast path while those from another thread take the shared one; the basis is the observation that “most objects are only accessed by a single thread, even in multi-threaded programs”. What this means for an object's lifetime the lesson checks by running
bench/gc/freethreaded.pyon 3.13t and 3.14t, not by paraphrase.https://peps.python.org/pep-0703/ - weakref — Weak referencesOfficial documentation. The definition the liveness checks in this lesson's runs rest on: “A weak reference to an object is not enough to keep the object alive.” And WeakMethod: “Since a bound method is ephemeral, a standard weak reference cannot keep hold of it.”https://docs.python.org/3/library/weakref.html