Cyclic Garbage Collection
CPython's second memory-reclamation mechanism finds and reclaims groups of objects that reference each other in a cycle — the one case plain reference counting can never handle alone.
Reference counting reclaims most unreachable objects immediately, but it has one structural blind spot: a group of objects that reference each other in a cycle keeps every member's count above zero forever, even once nothing outside the cycle can reach any of them. CPython's cyclic garbage collector exists specifically to find and break these cycles. This entire mechanism — its existence, its algorithm, its tuning — is a CPython implementation detail, exposed to Python code through the standard gc module.
What a Reference Cycle Is
A reference cycle occurs when a chain of references loops back on itself: A references B, B references A (a direct two-object cycle), or longer chains like A -> B -> C -> A. Cycles are extremely common in ordinary Python code, often without the programmer intending one at all — doubly linked lists, trees with parent pointers, and objects that register themselves with a manager they also hold a reference to all create cycles naturally.
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
self.parent = None
def add_child(self, child):
child.parent = self # child -> parent
self.children.append(child) # parent -> child (cycle!)
root = TreeNode("root")
leaf = TreeNode("leaf")
root.add_child(leaf)
# root.children contains leaf, and leaf.parent is root -- a two-object cycleWhy Reference Counting Alone Fails Here
Once every external reference to root and leaf is dropped, nothing in the rest of the program can reach either object — they are genuinely unreachable, and by the language's own definition should be eligible for reclamation. But their reference counts never reach zero on their own, because each object still holds a live reference to the other.
del root
del leaf
# Neither TreeNode's refcount reaches zero: root is still referenced
# by leaf.parent, and leaf is still referenced by root.children (which
# is itself only reachable through root -- but the two keep each other
# artificially "alive" from refcounting's point of view).How the Cyclic Collector Works, Conceptually
CPython implementation detail: rather than tracking every object in the program, CPython's cyclic collector only tracks 'container' objects — ones that can hold references to other objects, like lists, dicts, sets, and instances of user-defined classes. Periodically, it walks this tracked set, looking for groups of objects whose only remaining references come from within the group itself (i.e., no reference from outside the group keeps the group's count above what internal references alone would produce). Any such group is unreachable from the rest of the program and gets collected together, all at once.
The exact algorithm (this is a simplified description of a 'trial deletion'/mark-and-sweep-style approach), its generational structure, and its collection thresholds are all CPython implementation internals that have changed across versions and should not be relied on for program correctness.
Algorithm sketch (conceptual, not exact CPython source):
1. For every tracked container object, start from its real refcount.
2. Temporarily subtract references that come from OTHER tracked
objects within the same candidate set (simulating "what if this
whole group were removed?").
3. Any object whose adjusted count drops to zero is only kept alive
by other members of the group -- i.e. the group is an unreachable
cycle from the rest of the program's point of view.
4. Reclaim every object in such a group together.The gc Module
The standard library's gc module is your interface to CPython's cyclic collector — you can trigger it manually, inspect what it's tracking, and tune or disable it entirely.
Under normal circumstances you should not need to call gc.collect() manually — the collector runs automatically, triggered by allocation-count thresholds. Manual calls are mainly useful for benchmarking, for testing cleanup logic deterministically, or as a last resort in memory-sensitive long-running processes.
import gc
gc.collect() # force a full collection cycle right now, returns
# the number of unreachable objects it found
gc.isenabled() # True by default -- automatic periodic collection is on
gc.disable() # turn off automatic cyclic collection
gc.enable() # turn it back on
print(gc.get_count()) # (gen0, gen1, gen2) object counts -- CPython's
# generational collector groups objects by age
print(gc.get_threshold()) # collection thresholds per generationUnreachable Cycles vs. Reachable Cycles
A cycle by itself is not a problem — plenty of perfectly normal, actively-used data structures contain cycles (a parent-child tree, a doubly linked list) and stay reachable the whole time your program uses them, just like any other object graph. The cyclic collector only reclaims a cycle once the entire group has become unreachable from everything outside it — reachable cycles are left completely alone, exactly as they should be.
root = TreeNode("root")
leaf = TreeNode("leaf")
root.add_child(leaf)
# root and leaf form a cycle, but 'root' is still a reachable global
# name -- the collector will never touch this tree while root existsLimitations of the Cyclic Collector
The collector is powerful but not exhaustive. Only objects the collector actually tracks (container types) participate — plain immutable scalars like int and str generally can't form cycles anyway, so this is rarely an issue in practice. More importantly, the collector runs periodically based on allocation thresholds, not instantly: a cycle can sit around unreclaimed for a while after becoming unreachable, so memory measured right after breaking a cycle may not reflect the reclamation yet until the next collection pass (or an explicit gc.collect()).
__del__ Complications with Cycles
Historically (before Python 3.4), objects in an unreachable cycle where any member defined a __del__ method could not be safely collected at all — CPython had no way to know a safe order to finalize them in, so they were placed in gc.garbage and left uncollected, a genuine leak. Python 3.4 (PEP 442) fixed this: __del__ methods on cyclic objects are now called safely, and the cycle is reclaimed. But subtleties remain worth knowing.
One remaining subtlety: if a __del__ method resurrects an object (makes it reachable again, e.g. by storing self somewhere), the finalization semantics get subtle and version-specific. Writing __del__ methods that touch other objects in a cycle, or that resurrect self, is a pattern to avoid — keep __del__ minimal (ideally just releasing a single non-Python resource) and prefer context managers for real cleanup logic.
class Node:
def __init__(self, name):
self.name = name
self.partner = None
def __del__(self):
print(f"{self.name} finalized")
a, b = Node("a"), Node("b")
a.partner = b
b.partner = a
del a, b
import gc
gc.collect() # both __del__ methods run; the cycle is fully reclaimed
# (guaranteed safe since Python 3.4 -- PEP 442)When to Think About Cyclic GC in Practice
For most application code, the cyclic collector is invisible machinery you never need to think about — it quietly cleans up the tree/graph/linked structures your code naturally creates. It becomes relevant when: profiling shows unexpectedly high memory tied up in cycles, you're writing long-running, memory-sensitive services and want predictable pause characteristics, or you're debugging __del__ methods that don't seem to run when expected.
# Weak references (see the Weak References topic) are the standard
# way to break a cycle intentionally, when a bidirectional relationship
# is only conceptually needed in one "strong" direction:
import weakref
class TreeNode:
def __init__(self, value):
self.value = value
self.children = []
self._parent_ref = None
@property
def parent(self):
return self._parent_ref() if self._parent_ref else None
def add_child(self, child):
child._parent_ref = weakref.ref(self) # weak: doesn't keep self alive
self.children.append(child) # strong: parent keeps child aliveMental Model
Picture reference counting as a fast, local janitor who reclaims any object the instant its last visitor leaves. But a small clique of objects that only ever visit each other never looks empty to that janitor — from the outside, the whole clique might be a ghost town nobody else can even enter, yet each member's own doorway still shows visitor traffic. The cyclic collector is a periodic building inspector who walks the whole property, identifies cliques that are cut off from the rest of the building (no outside doorway leads in), and clears them out together.
Quick Reference
gc.collect() -> force a collection pass now; returns count found
gc.disable() / enable() -> toggle automatic periodic collection
gc.isenabled() -> check current state
gc.get_count() -> (gen0, gen1, gen2) tracked-object counts
gc.get_threshold() -> allocation thresholds that trigger collection
gc.get_referrers(obj) -> what refers to obj
gc.get_objects() -> all tracked objects
Since Python 3.4 (PEP 442): __del__ on cyclic objects is safe and runs.
Cycles are only ever a problem when unreachable from the whole program.Key Takeaways
A reference cycle is a group of objects that reference each other, directly or through a chain, keeping their reference counts above zero forever without outside help.
CPython implementation detail: the cyclic garbage collector periodically scans tracked container objects and reclaims any group that has become unreachable as a whole, even though refcounting alone could never detect it.
Cycles are not inherently a problem — a cycle that's still reachable from the rest of the program is left completely alone; only unreachable cycles get collected.
The gc module lets you inspect, tune, force, and disable cyclic collection, though manual intervention is rarely needed in ordinary code.
Since Python 3.4 (PEP 442), __del__ methods on objects in a cycle run safely during collection; keep __del__ minimal and avoid resurrecting objects from it.
Weak references are the standard tool for intentionally avoiding a cycle in bidirectional relationships like parent/child trees or caches.
class Parent: def __init__(self): self.children = [] class Child: def __init__(self, parent): self.parent = parent # strong reference back to parent parent.children.append(self) # strong reference forward to child # every parent/child pair is a reference cycle by construction import weakref class Parent: def __init__(self): self.children = [] class Child: def __init__(self, parent): self.parent = weakref.ref(parent) # weak: doesn't keep parent alive parent.children.append(self) # strong: parent owns child
Bidirectional parent/child relationships naturally form cycles; the cyclic GC will still eventually reclaim them if they become globally unreachable, but making the 'back' reference weak avoids relying on the collector at all and lets refcounting alone manage the relationship.
class Resource: def __del__(self): self.other.cleanup() # touches ANOTHER object during finalization self.other = None class Resource: def __del__(self): # keep finalizers minimal and self-contained; release only # this object's own non-Python resource, if any if getattr(self, "_handle", None) is not None: self._handle.close()
A __del__ that reaches into another object (especially another member of a cycle) invites subtle ordering and resurrection issues during cyclic collection; keep finalizers minimal and prefer context managers for real cleanup logic.
import gc gc.disable() #... long-running program that creates many cyclic structures... # never re-enabling gc -- unreachable cycles accumulate unbounded import gc gc.disable() try: run_latency_sensitive_section() finally: gc.enable() gc.collect() # reclaim anything that piled up while disabled
Disabling the cyclic collector (e.g. to avoid GC pauses in a latency-sensitive section) stops cycles from ever being reclaimed while it's off; always re-enable it (and typically force a collection) once the sensitive section ends.
Why can't reference counting alone reclaim a reference cycle, even after the cycle becomes completely unreachable from the rest of the program?
Each object in the cycle still holds a live reference to at least one other object in the cycle, so every member's reference count stays above zero from those internal references alone, even though no reference from outside the cycle reaches any of them anymore.
At a conceptual level, how does the cyclic collector determine that a group of objects is an unreachable cycle rather than a normal, still-in-use structure?
It examines whether a group's members are kept alive only by references from within the group itself — if temporarily disregarding internal references would drop every member's count to zero, nothing outside the group can be reaching it, so the whole group is unreachable and gets reclaimed together.
Is a data structure that contains a cycle (like a tree with parent pointers) automatically a memory problem? Why or why not?
No — a cycle is only ever collected once it becomes entirely unreachable from the rest of the program; a tree with parent pointers that a live variable still references is left completely alone, just like any other actively used object graph.
What changed in Python 3.4 (PEP 442) regarding objects with __del__ methods that are part of a reference cycle?
Before 3.4, an unreachable cycle where any member defined __del__ could not be safely collected (no safe finalization order was known), so such cycles were placed in gc.garbage and effectively leaked; since 3.4, CPython can call __del__ methods on cyclic objects safely, so those cycles are now reclaimed rather than leaked.
Name two things the gc module lets you do, and give a realistic reason you might use each.
gc.collect() forces an immediate collection pass, useful for testing cleanup logic deterministically or reclaiming memory before a measurement; gc.disable()/gc.enable() toggle automatic periodic collection, useful for avoiding GC pauses during a latency-sensitive section (paired with re-enabling and collecting afterward).
How do weak references help avoid relying on the cyclic collector for a parent/child relationship?
Making the child-to-parent reference a weakref.ref instead of a normal strong reference means the child no longer keeps the parent's reference count elevated, so no cycle is formed in the first place; plain reference counting alone can then reclaim either object as soon as it becomes unreachable, without waiting for a periodic cyclic collection pass.
Write a demonstration that creates a two-object reference cycle with __del__ methods on each, deletes the external names, and shows (via gc.collect()'s return value and prints) that the cycle is only reclaimed once the collector runs.
import gc gc.disable() # isolate the demo from automatic background collection class Node: def __init__(self, name): self.name = name self.partner = None def __del__(self): print(f"{self.name} finalized") a, b = Node("a"), Node("b") a.partner, b.partner = b, a del a, b print("before collect: nothing finalized yet (refcounting can't break the cycle)") found = gc.collect() print(f"gc.collect() found and reclaimed {found} unreachable objects")
Deleting the names a and b removes the only external references, but each Node still references the other, so refcounting keeps both alive with no __del__ output. gc.collect() runs the cyclic collector, which identifies the pair as an unreachable group, finalizes both (printing their __del__ output), and reports how many objects it reclaimed.
What does this print, and what does gc.is_tracked(a1) tell you before the cycle is broken?
It prints True (a1 is a container-like user-defined object, so the cyclic collector tracks it) and then True (gc.collect() found and reclaimed at least one unreachable object — the a1/a2 cycle). gc.is_tracked reflects whether an object participates in cyclic-collection bookkeeping at all, which most mutable container and instance objects do.
Write a Node class for a doubly linked list where each node holds a strong 'next' reference but a weak 'prev' reference (using weakref), demonstrating that this avoids forming a reference cycle between adjacent nodes.
import weakref class Node: def __init__(self, value): self.value = value self.next = None # strong reference forward self._prev_ref = None # weak reference backward @property def prev(self): return self._prev_ref() if self._prev_ref else None @prev.setter def prev(self, node): self._prev_ref = weakref.ref(node) if node is not None else None n1, n2 = Node("first"), Node("second") n1.next = n2 n2.prev = n1 print(n2.prev.value) # "first" -- still works via the weak reference del n1 print(n2.prev) # None -- n1 was reclaimed immediately by refcounting, # since nothing but a weak reference pointed to it
Because n2's reference to n1 is weak, n1's strong reference count is not kept elevated by n2 at all, so once the external name n1 is deleted, n1's refcount drops to zero immediately and it is reclaimed by plain reference counting — no cycle, and no need for the cyclic collector to intervene. n2.prev correctly returns None afterward, since the weak reference's target is gone.
import gc
class A:
def __init__(self):
self.ref = None
a1 = A()
a2 = A()
a1.ref = a2
a2.ref = a1
print(gc.is_tracked(a1))
del a1, a2
print(gc.collect() > 0)