Back to Blog
Python

Python Garbage Collection: How It Works

python garbage collection: Learn how Python's garbage collection works: reference counting, cyclic references, generational GC, and tuning the gc module for better mem...

garbage collectionmemory managementgc modulereference countingcyclic references
Diagram illustrating Python's garbage collection process with reference counting and cyclic reference detection.

Python garbage collection is a two-part system: reference counting for immediate cleanup and a cyclic garbage collector for objects that reference each other in cycles. Understanding both mechanisms is essential for writing memory-efficient Python and for diagnosing memory leaks that reference counting alone cannot catch.

Reference Counting: The First Line of Defense

Every Python object keeps a count of how many references point to it. When you assign an object to a variable, pass it to a function, or store it in a container, the reference count increments. When a reference goes out of scope or is deleted, the count decrements. When it reaches zero, CPython deallocates the object immediately.

import sys obj = [] print(sys.getrefcount(obj)) # 2: one from obj, one from getrefcount argument

The sys.getrefcount() function returns the actual count plus one because the argument itself temporarily holds a reference. This immediate deallocation is deterministic: as soon as the last reference disappears, the memory is reclaimed. That is why Python does not suffer from the same memory bloat as some other garbage-collected languages when objects are short-lived.

Reference counting works well for most objects, but it fails when objects reference each other in a cycle. Consider two objects that point to each other. Even if no external references remain, their reference counts never drop to zero because each holds a reference to the other. This is where the cyclic garbage collector becomes necessary.

The Problem of Cyclic References

A cyclic reference occurs when a group of objects references each other, forming a loop. A common example is a parent-child relationship where the parent holds a list of children and each child holds a reference back to its parent.

class Node: def __init__(self, name): self.name = name self.parent = None self.children = [] parent = Node("parent") child = Node("child") parent.children.append(child) child.parent = parent # Remove external references del parent del child

After the del statements, the two objects still reference each other. Their reference counts are both 1, so they are never freed by reference counting. If such cycles accumulate, memory usage grows indefinitely. The cyclic garbage collector exists to find and collect these unreachable cycles.

How the Cyclic Garbage Collector Works

CPython's cyclic garbage collector is a generational collector that runs periodically. It tracks container objects—objects that can hold references to other objects, such as lists, dictionaries, tuples, and class instances. It does not track simple immutable objects like integers or strings because they cannot participate in cycles.

The collector divides tracked objects into three generations. New objects go into generation 0. When an object survives a collection, it is promoted to the next generation. The collector runs more frequently on younger generations because most objects die young.

The algorithm works by finding objects that are part of a cycle and have no external references. It does this by temporarily removing references and checking whether the objects become unreachable. If they do, the whole cycle is deallocated. This process is called cyclic garbage collection.

The gc Module: Inspecting and Controlling Collection

The gc module provides functions to interact with the cyclic garbage collector. You can enable or disable collection, trigger a collection manually, and inspect what objects are tracked.

import gc # Check if GC is enabled print(gc.isenabled()) # True # Force a full collection gc.collect() # Get the number of objects tracked by generation print(gc.get_count()) # (0, 0, 0) after a fresh collection

gc.collect() performs a full collection across all generations and returns the number of unreachable objects that were freed. This is useful when you know you have created many temporary cycles and want to reclaim memory immediately.

You can also disable the cyclic collector entirely with gc.disable(). This is rarely recommended because it can lead to unbounded memory growth if cycles are created. However, in some performance-critical applications, you might disable GC temporarily and run it manually at controlled points.

Tuning Garbage Collection Thresholds

The cyclic collector runs when the number of allocations minus deallocations exceeds a threshold. The thresholds are set per generation and can be adjusted with gc.set_threshold().

import gc # Set thresholds for generations 0, 1, and 2 gc.set_threshold(700, 10, 10)

The first threshold is for generation 0. When the number of tracked objects in generation 0 exceeds 700, a collection of generation 0 is triggered. The second and third thresholds control how often generations 1 and 2 are collected relative to the previous generation. For example, a threshold of 10 means generation 1 is collected every 10 generation-0 collections.

GenerationDefault ThresholdMeaning
0700Number of allocations minus deallocations
110Number of generation-0 collections before a gen-1 collection
210Number of gen-1 collections before a gen-2 collection

Lowering the generation-0 threshold makes the collector run more often, which can reduce peak memory usage but adds overhead. Raising it reduces collection frequency, which may improve performance but can let memory grow between collections. The optimal setting depends on your workload. For long-running processes with many temporary objects, you might want a lower threshold. For short scripts, the default is usually fine.

Common Pitfalls and Performance Considerations

One common mistake is assuming that gc.collect() is needed after every large operation. In most cases, the automatic collector handles cycles well. Overusing gc.collect() can hurt performance because a full collection scans all tracked objects.

Another pitfall is creating cycles unintentionally with closures or callbacks. For example, a class instance that stores a lambda referencing itself can create a cycle. Using weak references (weakref module) can break cycles when you do not need a strong reference.

import weakref class Node: def __init__(self): self._parent = None @property def parent(self): return self._parent @parent.setter def parent(self, node): self._parent = weakref.ref(node) if node is not None else None

Performance-wise, the cyclic collector adds overhead to allocation and deallocation because it tracks container objects. For applications that allocate millions of short-lived objects, this overhead can become noticeable. In such cases, you can disable GC during the hot path and enable it later, but you must be careful to avoid memory leaks.

Debugging Memory Leaks with gc

The gc module can help identify objects that are not being collected. Use gc.get_objects() to list all tracked objects, and gc.get_referrers() to find what references a specific object.

import gc # Force a collection to clean up unreachable objects gc.collect() # Find all objects of a specific type leaked_nodes = [obj for obj in gc.get_objects() if isinstance(obj, Node)] print(len(leaked_nodes)) # For a specific object, see what references it for ref in gc.get_referrers(leaked_nodes[0]): print(ref)

You can also use gc.DEBUG_SAVEALL to have the collector save unreachable objects in gc.garbage instead of freeing them. This is useful for inspecting what would have been collected.

import gc gc.set_debug(gc.DEBUG_SAVEALL) gc.collect() for obj in gc.garbage: print(type(obj), repr(obj))

Remember to clear gc.garbage after inspection to free the memory. This debugging approach is invaluable when you suspect a cycle is preventing deallocation. By combining gc.get_objects() with gc.get_referrers(), you can trace exactly why an object remains alive and then adjust your code to break the cycle or use weak references.

python garbage collection: Practical Usage and Code Examples | RYUSLOG DEV