Python gc: Control Garbage Collection

Published on: August 16, 2026
Reading time: 6 minutes
RAM module representing object memory management with Python gc

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, b

After 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.

  • Keep automatic collection enabled by default.
  • Measure before tuning thresholds.
  • Use lightweight callbacks for metrics.
  • Clear debug flags and gc.garbage afterward.
  • Use referrer APIs only for debugging.
  • Prefer explicit resource management.
  • Use tracemalloc to 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Lines of source code representing execution tracking with Python trace
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python trace: Track Execution

    Learn Python trace to count executed lines, follow runtime flow, list functions, combine coverage, and filter modules.

    Ler mais

    Tempo de leitura: 5 minutos
    16/08/2026
    Laptop with performance charts representing profile analysis with Python pstats
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pstats: Analyze Profiles

    Learn Python pstats to sort, filter, merge, and interpret cProfile data, including callers, callees, internal time, and cumulative time.

    Ler mais

    Tempo de leitura: 5 minutos
    15/08/2026
    Laptop with code representing executable examples tested with Python doctest
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python doctest: Test Examples

    Learn Python doctest to execute examples in docstrings and text files, normalize output, and integrate executable documentation with CI.

    Ler mais

    Tempo de leitura: 5 minutos
    15/08/2026
    Source code on screen representing class and function browsing with Python pyclbr
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pyclbr: Inspect Modules Safely

    Learn Python pyclbr to list classes, functions, methods, and nested definitions without importing or executing the target module.

    Ler mais

    Tempo de leitura: 5 minutos
    15/08/2026
    Monitor with binary code representing Python bytecode opcode instructions
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python opcode: Explore Bytecode

    Learn Python opcode to map bytecode instructions, arguments, jumps, caches, and stack effects through the documented dis APIs.

    Ler mais

    Tempo de leitura: 5 minutos
    15/08/2026
    Developer investigating memory usage with Python tracemalloc
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tracemalloc: Find Memory Leaks

    Use Python tracemalloc to compare snapshots, locate memory growth, and investigate leaks in long-running applications.

    Ler mais

    Tempo de leitura: 5 minutos
    15/08/2026