Reference cycles and generational cyclic GC
A reference cycle exists when objects reference each other directly or indirectly (a list containing itself, parent and child pointing at each other, a closure capturing the object that owns it). Each object keeps a non-zero reference count from the others, so pure reference counting never reaches zero even after the program loses all external references. CPython therefore adds a supplementary cycle collector, exposed through the gc module.
How it works: the collector only tracks container objects (lists, dicts, sets, class instances and so on); ints and strings cannot form cycles and are skipped. For a generation being collected it copies each tracked object's refcount, subtracts the references that come from other tracked objects in the same set, and any object left with a count above zero is reachable from outside. Everything reachable from those roots is kept; the rest is unreachable garbage and gets finalized and freed. Objects are placed into three generations; new objects start young and survive into older generations, which are collected less often, based on the generational hypothesis that most objects die young. Default thresholds are visible with gc.get_threshold() (classically 700, 10, 10).
Trade-offs and alternatives: cycle collection is not free; collections cause latency pauses that grow with the number of tracked objects. Better than relying on GC is not creating cycles: use weakref for back-pointers (child to parent, observer lists, caches). Teams with strict latency or memory-sharing needs sometimes call gc.disable() and gc.freeze() after startup (as Instagram famously described), but then you take responsibility for avoiding cycles and for occasional manual collection. gc.disable() as a general 'speed-up' is a common mistake because leaks follow.
Version-dependent details: since 3.4 (PEP 442) objects with del in cycles can be collected safely, whereas in Python 2 they were uncollectable. The incremental GC was developed during the 3.13 cycle, reverted before the 3.13 release, and later released in 3.14, so check the release notes of your target version. In the free-threaded build the collector works differently. Also, a gc.collect() count is a diagnostic, not a guarantee of how much memory returned to the OS.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience