Python gc exposes the interface to CPython’s cyclic garbage collector. CPython already releases most objects through reference counting, but cycles such as “object A points to B and B points back to A” may remain even after the application loses every external reference. The supplemental collector finds those unreachable groups and attempts to reclaim them.
Most applications should leave the default behavior unchanged. The API becomes valuable for memory-leak investigation, long-running services, pre-fork servers, tests involving cyclic graphs, and observability of collection pauses.
Reference counting and cycles
When the final reference to an ordinary object disappears, CPython generally destroys it immediately. A cycle prevents the counters from reaching zero.
class Node:
def __init__(self, name):
self.name = name
self.next = None
a = Node("a")
b = Node("b")
a.next = b
b.next = a
del a, bAfter del, application code cannot reach the nodes, but they still reference each other. A later cyclic collection can remove them.
Check whether automatic collection is enabled
import gc
print(gc.isenabled())gc.enable() turns automatic collection on and gc.disable() turns it off. Disabling the collector does not disable reference counting; it only suspends cycle detection.
Do not disable collection without evidence
An application that has proven it creates no cycles may temporarily disable the collector around a latency-sensitive section. This requires measurement and careful restoration.
was_enabled = gc.isenabled()
try:
gc.disable()
run_controlled_section()
finally:
if was_enabled:
gc.enable()If any dependency creates cycles during the interval, they remain until a later collection. Do not apply this as a generic optimization.
Force a collection
gc.collect() runs a full collection by default and returns the total of collected and uncollectable objects.
processed = gc.collect()
print(f"Objects processed: {processed}")Frequent manual calls can reduce throughput. Use them in tests, diagnostics, and carefully designed lifecycle points rather than after every request.
Select a generation
The collector groups objects according to how many collection sweeps they survive. New objects begin in the youngest generation and survivors move to older generations.
gc.collect(0)
gc.collect(1)
gc.collect(2)Intermediate-generation behavior changed during the Python 3.14 release series. Code that depends on generational details must be tested on the exact runtime version. Full collections also clear several internal free lists, although some objects, notably floats, may remain cached.
Read collection statistics
gc.get_stats() returns one dictionary per generation with collection count, collected objects, and uncollectable objects.
for generation, data in enumerate(gc.get_stats()):
print(generation, data)Observe trends over comparable workloads. One reading does not establish a leak. Correlate the data with process memory and request volume.
Inspect counters and thresholds
print(gc.get_count())
print(gc.get_threshold())get_count() reports current allocation counters. get_threshold() shows the limits used by the automatic heuristic.
Tune thresholds carefully
gc.set_threshold() changes collection frequency. Setting the first threshold to zero disables automatic collection.
old = gc.get_threshold()
try:
gc.set_threshold(1000, 15, 15)
run_workload()
finally:
gc.set_threshold(*old)Lower thresholds collect more often and can reduce temporary growth, but they add overhead. Higher thresholds reduce pauses and may increase memory. Free-threaded builds also consider process-memory growth and net allocations.
Observe collections with callbacks
gc.callbacks contains functions invoked before and after each collection.
import time
started = {}
def observe(phase, info):
generation = info["generation"]
if phase == "start":
started[generation] = time.perf_counter()
else:
duration = time.perf_counter() - started.pop(generation, 0)
print(
generation,
info["collected"],
info["uncollectable"],
duration,
)
gc.callbacks.append(observe)Callbacks run during a sensitive operation. Keep them fast and avoid networking, locks, logging storms, and excessive allocations. Remove the callback after the diagnostic period.
Use debug flags
gc.set_debug() enables information on stderr.
gc.set_debug(gc.DEBUG_STATS)DEBUG_COLLECTABLE and DEBUG_UNCOLLECTABLE print objects found. Output can be enormous and may expose sensitive data, so use it only in a controlled environment.
DEBUG_LEAK and DEBUG_SAVEALL
DEBUG_LEAK combines several flags and includes DEBUG_SAVEALL. In that mode, unreachable objects are appended to gc.garbage instead of being freed.
gc.set_debug(gc.DEBUG_LEAK)
gc.collect()
print(len(gc.garbage))This intentionally increases memory. After inspection, restore flags, clear the list, and collect again.
gc.set_debug(0)
gc.garbage.clear()
gc.collect()Understand gc.garbage
Since PEP 442, ordinary Python objects with __del__() normally remain collectible even in cycles. The list is usually empty except for specific extension types or when DEBUG_SAVEALL is active.
Do not ignore a nonempty list. Record safe type information without serializing arbitrary objects or invoking their methods.
Check whether an object is tracked
gc.is_tracked() reports participation in cyclic collection.
print(gc.is_tracked(10))
print(gc.is_tracked([]))
print(gc.is_tracked({"key": 1}))Atomic objects are generally not tracked. Some simple containers may be untracked as an optimization and become tracked when they receive complex values.
List tracked objects
gc.get_objects() returns tracked objects, optionally from one generation.
objects = gc.get_objects()
print(len(objects))The list can be huge, temporarily increases memory, and exposes internal application state. Use it in a diagnostic worker, filter by type, and never publish raw contents through an unprotected admin endpoint.
Find referrers
gc.get_referrers(target) returns tracked containers that directly reference the target.
gc.collect()
referrers = gc.get_referrers(target)
for item in referrers:
print(type(item))Results may include debugging frames, the inspection code itself, and partially constructed objects. The official documentation recommends using this function for debugging only. Avoid calling methods on returned objects.
Find referents
gc.get_referents() returns objects visited through the C-level tp_traverse protocol.
for item in gc.get_referents(target):
print(type(item))The result is not guaranteed to include every semantically reachable object. It contains references needed by the collector for cycle detection.
Finalization and resurrection
gc.is_finalized() reports whether an object’s finalizer has run. A problematic __del__() may resurrect its object by storing it again.
resurrected = None
class Lazarus:
def __del__(self):
global resurrected
resurrected = self
obj = Lazarus()
del obj
gc.collect()
print(gc.is_finalized(resurrected))Avoid complex logic in __del__(). Prefer context managers, explicit close(), and weakref.finalize(). See the Python weakref guide.
Freeze before fork
gc.freeze() moves tracked objects into a permanent generation that future collections ignore. In servers that call fork() without exec(), this can improve copy-on-write sharing.
gc.disable()
load_application()
gc.freeze()
pid = os.fork()
if pid == 0:
gc.enable()The pattern requires deliberate architecture: disable early in the parent, freeze immediately before fork, and enable in children. Do not use it on platforms or deployment models that do not follow this process.
Unfreeze when required
gc.unfreeze() returns permanent objects to the oldest generation. gc.get_freeze_count() reports how many are frozen.
print(gc.get_freeze_count())
gc.unfreeze()Unfreezing may lead to a large later collection. Measure both memory and latency.
GC and memory leaks
Not all memory growth comes from uncollected cycles. Caches, queues, registries, log buffers, modules, pools, and globals may retain objects legitimately. The collector cannot free objects that remain reachable.
Use Python tracemalloc to compare allocation snapshots, then use referrer inspection for suspicious types.
Count objects by type
from collections import Counter
counts = Counter(type(obj).__name__ for obj in gc.get_objects())
for name, total in counts.most_common(20):
print(name, total)Take snapshots at equivalent lifecycle points. The analysis itself creates objects and can distort small differences.
Avoid unnecessary cycles
Callbacks, closures, observers, parent-child graphs, and tasks create cycles easily. Use weak references when a relationship does not imply ownership. Remove listeners and pending tasks when components shut down.
A broader process is described in the Python memory leak guide.
Do not collect after every request
A full collection per request usually reduces throughput and increases latency. If memory falls only after manual collection, investigate why the application creates many cycles or why default thresholds do not match the workload.
Security and auditing
get_objects(), get_referrers(), and get_referents() emit audit events and can reveal secrets. Restrict them to diagnostic environments, protected logs, and trusted administrators.
Recommended practices
- Keep automatic collection enabled by default.
- Measure before tuning thresholds.
- Use lightweight callbacks for metrics.
- Clear debug flags and
gc.garbageafterward. - Use referrer APIs only for debugging.
- Prefer explicit resource management.
- Use
tracemallocto locate allocation growth. - Test on the exact Python release.
Conclusion
Python gc lets advanced applications observe and control the cyclic collector that complements CPython reference counting. Statistics, callbacks, debug flags, reference inspection, and freezing support difficult memory investigations.
Use the interface carefully because introspection adds overhead and exposes internal objects. Consult the official gc documentation and the CPython garbage collector design guide.







