The gc Module
The
gcmodule is the public Python interface to CPython’s cyclic garbage collector — the part of memory management that reference counting cannot handle, namely reference cycles. It is deliberately thin: it does not implement collection (that lives in C, in the cyclic collector), but exposes a control surface over it — introspection (get_objects,get_referrers,get_referents,is_tracked), control (collect,enable/disable,get_count,get_stats,freeze/unfreeze), a callbacks hook list run before and after every collection, agarbagelist of objects the collector could not free, and debug flags routed tosys.stderr(gc module docs). Because the cyclic collector only ever sees container objects (the only things that can form cycles), almost everything ingcis scoped to GC-tracked containers; atomic objects like plain integers and strings are invisible to it.
This note is the API reference and the map from each Python call down to the C-level collector. It deliberately defers threshold tuning to GC Thresholds and Tuning (the set_threshold/get_threshold knobs and the generational-hypothesis reasoning behind them) and the cycle-detection algorithm itself to The Cyclic Garbage Collector. Where the API semantics shifted across the 3.14 release line — because of the incremental collector and its reversion — the version notes are called out inline, because they are an easy way to write code that silently behaves differently on 3.14.4 versus 3.14.5.
Verified version notes (2026-06-01)
The “Changed in version 3.14 … Changed in version 3.14.5 …” notes for
gc.collect,gc.get_objects, andgc.set_thresholdare verified verbatim against the live gc docs:collect— 3.14 “generation=1performs an increment of collection”, 3.14.5 “generation=1performs collection of the middle generation”;get_objects— 3.14 “Generation 1 is removed”, 3.14.5 “Generation 1 is reintroduced to maintain GC behavior from 3.13”;set_threshold— 3.14 “threshold2 is ignored”, 3.14.5 “threshold2 is restored to match Python 3.13 behavior”. They describe the incremental→generational revert covered in The Incremental GC and Its Reversion.
Mental Model
Picture two layers. The bottom layer is the C collector: it maintains, per generation, doubly linked lists of every tracked container object, a running allocation/deallocation counter per generation, the trial reference-count machinery that finds unreachable cycles, and a list of “uncollectable” objects it gave up on. The top layer is the gc module — a set of Python functions that read and poke that C state. You never manipulate the linked lists directly; you ask gc to run a collection, report what it tracks, or hand you the count since the last sweep.
The single most important fact for using the module correctly is what “tracked” means. The collector only tracks objects that could participate in a cycle — containers. An int, str, or float can never refer back to something that refers to it, so the collector never tracks it and gc.is_tracked() returns False for it. Even some containers are untracked as an optimization: a dict whose keys and values are all atomic is untracked until it gains a non-atomic member (gc module docs). This is why gc.get_objects() returns “every tracked container,” not “every object” — the heap of small immutables is simply not the collector’s concern.
flowchart LR subgraph PY["Python layer — gc module"] I["introspect:<br/>get_objects · is_tracked<br/>get_referrers · get_referents<br/>get_count · get_stats"] C["control:<br/>collect · enable/disable<br/>freeze/unfreeze<br/>set_threshold"] H["hooks & sinks:<br/>callbacks list<br/>garbage list<br/>set_debug → stderr"] end subgraph CC["C layer — cyclic collector"] L["per-generation<br/>linked lists of<br/>tracked containers"] T["trial-refcount<br/>cycle detection<br/>(gc_refs)"] U["uncollectable<br/>objects"] end I --> L C --> L C --> T H --> T H --> U U --> H
Diagram: the gc module (left) is a control surface; the real work happens in the C collector (right). Introspection calls read the per-generation tracked-object lists; control calls drive collection and the allocation counters; the callbacks list and garbage list straddle the boundary, with the C collector invoking Python callbacks around each sweep and depositing unfreeable objects into gc.garbage. The insight: every gc function is a façade — knowing which C structure it touches tells you its cost and its caveats (e.g., why get_referrers is slow and can see half-dead objects).
Introspection
gc.get_objects(generation=None) returns a list of all objects the collector currently tracks, excluding the returned list itself (gc module docs). It walks the C per-generation linked lists and packages them into a Python list. With no argument it returns every tracked container across all generations; with a generation integer it returns only that generation’s list. This is the workhorse of leak hunting — diff two get_objects() snapshots to see what accumulated. It raises the auditing event gc.get_objects with the generation argument, so a sandbox can observe the introspection. Version notes matter here: the generation parameter was added in 3.8; under the 3.14 incremental collector “Generation 1 removed,” and 3.14.5 reintroduced it to “maintain 3.13 GC behavior” (gc module docs; see The Incremental GC and Its Reversion).
gc.get_referrers(*objs) answers “who points at these?” — it returns the list of tracked objects that directly refer to any argument. Because it can only see GC-tracked containers, a referrer that is an extension type not supporting GC will not be found. Two sharp edges: objects in not-yet-collected cycles can appear even though they are logically dead (call gc.collect() first to prune them), and returned objects may be under construction and temporarily in an invalid state — so the docs label it debugging-only (gc module docs). It raises the gc.get_referrers auditing event.
gc.get_referents(*objs) is the inverse — “what do these point at?” It returns the objects directly reachable from the arguments, computed by invoking each argument’s C-level tp_traverse slot (the same traversal function the collector itself uses to walk references). Because tp_traverse is what each type chooses to report, the result “may not return all directly reachable objects” — e.g., small integers may or may not appear depending on the type’s traversal (gc module docs). This direct mapping to tp_traverse is the cleanest illustration of the façade idea: get_referents is the collector’s edge-walking step, exposed to Python. It raises the gc.get_referents auditing event.
gc.is_tracked(obj) returns True if the object is currently tracked. The documented examples make the rule concrete:
>>> gc.is_tracked(0) # False — int is atomic
>>> gc.is_tracked("a") # False — str is atomic
>>> gc.is_tracked([]) # True — list is a container
>>> gc.is_tracked({}) # False — empty dict, optimized to untracked
>>> gc.is_tracked({"a": 1}) # True — dict with content becomes trackedLine by line: an int and a str are atomic and never tracked. An empty list is tracked the moment it exists, because a list can hold anything (including a reference back to itself). An empty dict is optimized to untracked; the same dict becomes tracked once it holds a key/value, because now it could reference a container (gc module docs). Added in 3.1. The takeaway for cycle reasoning: if is_tracked is False, the object can never be the cause of a cyclic leak.
gc.is_finalized(obj) returns True if the collector has already run the object’s finalizer (its __del__); added in 3.9 (gc module docs). This matters for the resurrection edge cases discussed in Finalizers and the del Method — an object whose __del__ already ran will not run it again even if it is resurrected and dies a second time.
Control
gc.collect(generation=2) forces a collection and returns the number of unreachable objects found (the sum of collected plus uncollectable). With no argument it runs a full collection of the oldest generation; when collecting generation 2, the free lists for built-in types are also cleared. It raises ValueError for an invalid generation (gc module docs). The generation=1 argument is the one that moved with the 3.14 saga: “Changed in 3.14: generation=1 performs an increment of collection” (the incremental scheme), then “Changed in 3.14.5: generation=1 performs collection of the middle generation” (the revert restored the 3.13 meaning) (gc module docs). Code that calls gc.collect(1) therefore does subtly different work on 3.14.4 versus 3.14.5 — see The Incremental GC and Its Reversion.
gc.disable() / gc.enable() / gc.isenabled() toggle and report automatic collection. Disabling does not stop reference counting (objects with zero refcount are still freed immediately); it only stops the threshold-triggered cyclic sweeps. The common pattern is to disable() during a latency-critical phase and collect() manually at a safe point, or to disable() before a fork() (see freeze below). Manual gc.collect() works regardless of the enabled state.
gc.get_count() returns (count0, count1, count2): the current per-generation allocation-minus-deallocation tallies that drive automatic collection. When count0 crosses threshold0, an automatic collection of generation 0 fires. This is a live counter, not a cumulative statistic — it resets as collections run (gc module docs).
gc.get_stats() is the cumulative counterpart: a list of three per-generation dictionaries (one per generation), each with collections (how many times this generation was collected), collected (total objects reclaimed), and uncollectable (total objects found unreachable but unfreeable, moved to gc.garbage), accumulated since interpreter start (gc module docs). Added in 3.4. Where get_count answers “how close are we to the next sweep?”, get_stats answers “how much work has the collector done overall?” — the natural input to a long-running-process health metric.
gc.freeze() / gc.unfreeze() / gc.get_freeze_count() manage a permanent generation. freeze() moves every currently tracked object into a permanent generation that future collections ignore; unfreeze() moves them back into the oldest generation; get_freeze_count() reports how many objects are frozen (gc module docs). Added in 3.7. The canonical use is the pre-fork copy-on-write optimization: the docs prescribe gc.disable() early in the parent, gc.freeze() just before fork(), then gc.enable() early in the child. Freezing prevents the collector from writing to the GC headers of long-lived parent objects after the fork — writes that would otherwise dirty shared copy-on-write pages and inflate every worker’s RSS. This is a standard tactic in pre-forking servers (Gunicorn, Instagram’s well-known disable-GC work).
set_threshold / get_threshold exist here too but their semantics and tuning belong to GC Thresholds and Tuning — note only that threshold2 was “ignored” under the 3.14 incremental collector and “restored to match Python 3.13 behavior” in 3.14.5 (gc module docs).
The callbacks List
gc.callbacks is a plain Python list of functions the collector invokes immediately before and after each collection (gc module docs, added in 3.3). Each callback is called as callback(phase, info):
phaseis"start"(collection about to begin) or"stop"(collection finished).infois a dict with"generation"(the oldest generation being collected) always present, plus, whenphase == "stop","collected"(objects successfully freed) and"uncollectable"(objects that could not be freed and were appended togc.garbage).
A worked timing-and-stats hook:
import gc, time
_start = {}
def gc_timer(phase, info):
gen = info["generation"]
if phase == "start":
_start[gen] = time.perf_counter()
elif phase == "stop":
elapsed = time.perf_counter() - _start.pop(gen, time.perf_counter())
print(f"gen {gen}: {elapsed*1e3:.2f} ms, "
f"collected={info['collected']} "
f"uncollectable={info['uncollectable']}")
gc.callbacks.append(gc_timer)Line by line: _start stashes the start timestamp keyed by generation, because collections of different generations can be reported and we want per-generation timing. On "start" we record perf_counter(); on "stop" we compute the elapsed pause and read collected/uncollectable straight out of info. Appending the function to gc.callbacks registers it; the collector iterates the list around every sweep. The documented use cases are exactly this — gathering GC statistics (how often each generation is collected, how long it takes) — and cleaning up an application’s own uncollectable types: a "stop" callback can inspect the freshly grown gc.garbage and break the cycles the collector refused to touch (gc module docs). Because callbacks run inside a collection, they should be fast and must not themselves trigger heavy allocation.
gc.garbage and Uncollectable Objects
gc.garbage is a list of objects the collector found unreachable but could not free (gc module docs). Under normal operation on modern Python it should be empty almost always, because PEP 442 (Python 3.4) made objects with a __del__ finalizer collectable even inside cycles: “Changed in version 3.4: Objects that have __del__() methods… no longer end up in gc.garbage.” The remaining way to populate it organically is a C extension type with a non-NULL tp_del slot — the old-style finalizer the collector cannot safely run inside a cycle.
Two behaviors make garbage a deliberate debugging tool:
DEBUG_SAVEALL. Setting this debug flag changes the rules: “all unreachable objects found will be appended togarbagerather than being freed,” turninggc.garbageinto a complete record of everything the last collection would have freed — invaluable for “what is leaking?” investigations (gc module docs).- Shutdown reporting. Since 3.2, a non-empty
gc.garbageat interpreter shutdown emits aResourceWarning(silent by default); withDEBUG_UNCOLLECTABLEset, the uncollectable objects are printed (gc module docs).
The relationship between gc.garbage and finalizers is the seam where this note touches Finalizers and the del Method: PEP 442’s whole point was to empty this list for ordinary Python objects, and what remains is the genuinely-stuck residue.
Debug Flags
gc.set_debug(flags) installs debugging flags whose output goes to sys.stderr; gc.get_debug() reads them back (gc module docs). The flags are bitmask constants:
gc.DEBUG_STATS— print collection statistics, useful for choosing thresholds (cross-reference GC Thresholds and Tuning).gc.DEBUG_COLLECTABLE— print each collectable object found.gc.DEBUG_UNCOLLECTABLE— print each uncollectable object found (those added togarbage); since 3.2 also dumps thegarbagelist at shutdown if non-empty.gc.DEBUG_SAVEALL— divert all unreachable objects intogc.garbageinstead of freeing them (see above).gc.DEBUG_LEAK— the convenience combination for leak hunting, defined asDEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL.gc.set_debug(gc.DEBUG_LEAK)is the one-liner that makes every collection retain and report everything it would have freed.
Failure Modes and Common Misunderstandings
get_referrerslies during construction. Because it can return half-built or logically-dead-but-uncollected objects, treating its output as ground truth in production logic is a bug; it is a debugger’s tool. Rungc.collect()first if you need only live referrers (gc module docs).- Disabling GC does not stop memory reclamation. A frequent misconception:
gc.disable()leaves reference counting fully active, so non-cyclic garbage is freed instantly. It only suspends automatic cyclic sweeps — cycles then accumulate until you callgc.collect()manually or re-enable. DEBUG_SAVEALLturns the collector into a leak. By design it stops freeing things. Forgetting to clear it (gc.set_debug(0)) in a long-running process is itself a memory leak.- Untracked containers fool leak hunts. Diffing
gc.get_objects()will miss accumulating atomic objects and untracked dicts entirely — they were never tracked. For those,sys.getsizeofplustracemallocis the right tool, notgc. - Version-skewed
collect(1). As covered above and in The Incremental GC and Its Reversion,gc.collect(1),gc.get_objects(1), and the thirdset_thresholdargument all changed meaning in 3.14.0–3.14.4 and changed back in 3.14.5 — code relying on them is not portable across that boundary.
See Also
- The Cyclic Garbage Collector — the C-level trial-refcount algorithm that
gc.collectdrives andget_referentsexposes. - Finalizers and the del Method — why PEP 442 emptied
gc.garbagefor__del__objects, and the resurrection rules behindgc.is_finalized. - GC Thresholds and Tuning —
set_threshold/get_thresholdsemantics and the generational-hypothesis tuning rationale (deliberately not duplicated here). - Generational Garbage Collection — what the three generations are and how
get_count/get_statsmap onto them. - The Incremental GC and Its Reversion — the 3.14 collector change that flipped several
gcAPI meanings and then flipped them back in 3.14.5. - Reference Cycles and How They Form — why a cyclic collector is needed on top of reference counting at all.
- Python Internals MOC → §7 Garbage Collection.