How Python Garbage Collection Works: Cycles, Generations & gc

How Python Garbage Collection Works: Cycles, Generations & gc — cover image

TL;DR

  • Reference counting handles many objects in CPython as soon as their reference count reaches zero.
  • It cannot, by itself, reclaim unreachable reference cycles because objects inside a cycle continue to reference one another.
  • The cyclic garbage collector in CPython finds groups of tracked, cycle-capable objects that are inaccessible.
  • Generational collection reduces work by checking younger objects more frequently than long-lived survivors.
  • The gc module provides tools such as gc.collect(), gc.get_count(), and gc.get_threshold() for inspecting and controlling the collector.
  • Memory is managed via garbage collection, not by outside resources. When cleanup needs to be done deterministically, use context managers.

Introduction

A Python object can become unreachable while its reference count is still greater than zero. That sounds contradictory—but reference cycles make it possible.

def create_cycle():
    first = []
    second = []

    first.append(second)
    second.append(first)

create_cycle()

The local names vanish after the function returns. However, the lists continue to make reference to one another:

List A ──> List B
  ▲                 │
  └───────┘

So reference counting alone cannot reclaim them.

This is where Python garbage collection comes in. CPython uses cyclic garbage collection alongside reference counting to identify tracked objects that form unreachable cycles. The key question changes from "How many references does this object have?" to "Can this object still be reached from a live part of the program?"

This article explains that mechanism step by step—from reference cycles and reachability to generations, the gc module, memory leaks, and deterministic resource cleanup.

What Is Garbage Collection in Python?

What Is Garbage Collection in Python?

Garbage Collection Meaning

Garbage collection is a memory-management procedure that finds objects that a program can no longer utilize and releases their memory for future usage.

But garbage does not mean "bad data."

Consider:

def load_user():
    user = {"id": 1, "name": "Ada"}
    return None

load_user()

If nothing else refers to user after the function finishes, that dictionary can become unreachable.

Now consider:

cache = {}
cache["result"] = [0] * 1_000_000

The list may be unnecessary from the application's perspective, but it is still reachable through cache. The collector cannot know that your application no longer wants it.

So:

unreachable object
        ↓
potential garbage

reachable but unwanted object
        ↓
application-level memory problem

Garbage collection is therefore about reachability, not whether an object contains useful-looking data.

Reference Counting and Garbage Collection

Reference Counting and Garbage Collection

Two complementary approaches are used by CPython:

Reference counting
        ↓
many objects can be reclaimed
when their count reaches zero

Cyclic garbage collection
        ↓
handles unreachable cycles
that reference counting cannot solve alone

Why Reference Counting Alone Is Not Enough

For a normal object:

items = [1, 2, 3]
items = None

The list's reference count may drop to 0 if there are no further references, in which case CPython may recover it.

But now:

a = []
b = []

a.append(b)
b.append(a)

del a
del b

The names disappear, but the object graph remains:

List A ──> List B
   ▲                 │
   └───────┘

There are still references between each list. As a result, their reference counts continue to be nonzero. However, neither list contains a live program reference. That is rubbish that is cyclical.

What Is a Reference Cycle?

When tracking references finally returns to an object previously observed, this is known as a reference cycle.

Self-Referential Object

items = []
items.append(items)

The structure is:

items ──> list
                   │
                   └──> same list

Python can display this recursive structure as:

[[...]]

The ... stops Python from growing the same object indefinitely.

Now:

del items

The list still has a reference to itself even if the name vanishes.

Its count cannot be reduced to zero by reference counting alone.

Two-Object Cycle

first = []
second = []

first.append(second)
second.append(first)

This produces:

first ──> List A ──> List B
           ▲                         │
           └──────────┘

While first and second exist, the cycle is reachable.

After:

del first
del second

if no other references exist, the entire cycle becomes unreachable and can be collected.

Reachability: The Key to Garbage Collection

What Does "Reachable" Mean?

An object is reachable when the program can get to it by following references from a live part of the program.

For example:

profile = {
    "name": "Ada",
    "skills": ["Python", "Math"]
}

Conceptually:

profile
    │
   ▼
 dict
  ├──> "Ada"
  └──> list
             ├──> "Python"
             └──> "Math"

The list is reachable because the program can follow:

profile → dictionary → list

Active local variables, module globals, live containers, closures, running frames, and interpreter-managed references are some potential places to start.

Reachable vs Unreachable Cycles

A cycle is not automatically garbage.

team = []
team.append(team)

The list is cyclic, but team still points to it.

Therefore:

print(len(team))
print(team[0] is team)

works normally.

If you later do:

team = None

and no other reference exists, the cycle becomes unreachable.

So the rule is:

reachable cycle
    → keep it

unreachable cycle
    → eligible for cyclic collection

Object Graphs and Cyclic Garbage

Python objects form object graphs.

Think of:

object = node
reference = connection

For:

profile = {
    "name": "Ada",
    "skills": ["Python", "Math"]
}

the graph is:

profile ──> dict
                     ├──> "Ada"
                     └──> list
                                ├──> "Python"
                                └──> "Math"

Now add:

profile["self"] = profile

The graph contains a cycle:

profile ──> dict
                      │
                      └──> same dict

Cycles are common in real applications. Parent-child relationships, graphs, linked structures, callbacks, and observer systems can all create them. A cycle is only a garbage-collection problem when the entire cycle becomes unreachable.

Why Containers Matter

Cyclic garbage collection is mainly concerned with objects that can hold references to other objects.

Common examples include:

  • list
  • dict
  • set
  • Tuples containing cycle-capable objects
  • User-defined objects
  • Functions and closures
  • Frames

Compare:

x = 10
name = "Ada"
flag = True

These simple atomic objects do not normally contain references that create cycles.

A list, however, can contain another list:

items = []
items.append(items)

That makes a cycle possible.

So the collector does not need to treat every Python object identically. It focuses on objects capable of participating in cyclic object graphs.

Tracked and Untracked Objects

CPython tracks objects that may participate in cyclic garbage collection.

You can inspect an object's tracking status:

import gc

print(gc.is_tracked([]))
print(gc.is_tracked(10))

Typically, a list is tracked while an integer is not.

CPython can also optimize tracking for certain containers. For example, a dictionary containing only atomic keys and values may not be tracked, while a dictionary containing a container can be tracked. The exact behavior is an implementation detail.

The important idea is:

Cyclic GC tracking is about whether an object can participate in a reference cycle—not whether the object uses memory.

How Does CPython Detect Cyclic Garbage?

The exact collector implementation is more sophisticated than simply "scan everything."

At a conceptual level, it works with tracked, cycle-capable objects and their relationships.

Imagine:

Live root ──> A ──> B
X ──> Y
▲         │
└────┘

A and B are reachable from a live root.

X and Y reference each other, but nothing live points into that group.

The collector needs to distinguish:

reachable group
      → keep
unreachable group
      → collect

Because of this, cyclic GC is essentially about reachability and object graphs rather than just reference-count variables.

Generational Garbage Collection

Checking every tracked object during every collection would be unnecessarily expensive.

CPython therefore uses a generational approach.

The basic observation is:

Objects that were created recently are more likely to become garbage than objects that have survived for a long time.

For example:

for line in lines:
    parts = line.split(",")
    process(parts)

The temporary parts list may be created and discarded quickly.

By contrast:

settings = load_settings()
routes = build_routes()

may create objects that remain alive for a long time. So the collector can spend more attention on younger objects and less on long-lived survivors.

How Objects Move Through Generations

The traditional model is:

Generation 0
     ↓ survives collection
Generation 1
     ↓ survives collection
Generation 2

New objects start in the youngest generation. Objects that survive collection move toward older generations.

This diagram is better viewed as the conceptual model rather than an unchangeable implementation contract because the current CPython implementation exhibits version-specific behavior around generations and thresholds. Changes to generation handling in recent editions are also documented in the current Python documentation.

The durable idea is:

young objects
    → checked more frequently
long-lived objects
    → checked less frequently

Automatic Garbage Collection

CPython can run cyclic garbage collection automatically. You normally do not need to call gc.collect() manually. The collector maintains internal state related to object allocation and deallocation and uses its configured thresholds to decide when collection should occur.

One implementation detail is the precise timing. Consequently, avoid writing code that assumes:

"This object will definitely be collected after this line."

Rather, consider:

"If this object becomes unreachable, it is eligible for eventual reclamation."

Garbage collection is a memory-management mechanism, not a timer.

The gc Module

Python provides the gc module for inspecting and controlling cyclic garbage collection:

import gc

It is useful for:

  • Triggering collection
  • Inspecting collection counters
  • Inspecting thresholds
  • Checking whether objects are tracked
  • Investigating reference relationships
  • Enabling or disabling automatic collection

These tools are most useful for debugging, experimentation, and measured tuning—not ordinary application logic.

gc.collect()

You can explicitly request a collection:

import gc

collected = gc.collect()
print(collected)

A practical experiment:

import gc

def create_cycle():
    first = []
    second = []

    first.append(second)
    second.append(first)

create_cycle()

print(gc.collect())

The important sequence is:

cycle created
     ↓
function ends
     ↓
local references disappear
     ↓
cycle becomes unreachable
     ↓
collector identifies it
     ↓
cycle can be reclaimed

Do not depend on the exact number returned. Other objects may be collected, automatic collection may already have occurred, and the result depends on the execution environment.

gc.get_count()

Use:

import gc

print(gc.get_count())

It returns the current collection counters as a tuple:

(count0, count1, count2)

The collector state is described by these values. They don't represent the whole amount of RAM used by Python.

gc.get_threshold()

You can inspect the configured thresholds:

import gc

print(gc.get_threshold())

The result is:

(threshold0, threshold1, threshold2)

When automatic cyclic collection takes place is influenced by these thresholds. Their precise behavior depends on the version and implementation.

gc.set_threshold()

Thresholds can be changed:

import gc

gc.set_threshold(1000, 10, 10)

This is an advanced tuning tool.

Do not change thresholds simply because a program uses a lot of memory. First determine whether objects are genuinely becoming unreachable or whether your program is deliberately keeping references to too much data.

A collector cannot fix an ownership problem.

Inspecting Object Relationships With gc

The module also provides:

gc.get_referrers(obj)
gc.get_referents(obj)

For example:

import gc

items = []
container = {"items": items}

print(gc.get_referrers(items))
print(gc.get_referents(container))

Conceptually:

container ──> items

get_referrers() looks for objects that directly refer to an object.

get_referents() looks at objects directly referenced by the supplied object, based on the traversal support exposed to the collector.

These are debugging tools. Their output can include temporary or interpreter-related objects, so they should not be used as ordinary application logic.

Garbage Collection and Memory Leaks

Garbage collection does not mean Python programs cannot have memory leaks.

Consider:

events = []

def record_event(event):
    events.append(event)

All those items are still achievable if the number of events continues to rise. They are rightly left alone by the collector.

Similarly:

cache = {}

def get_result(key):
    if key not in cache:
        cache[key] = expensive_operation(key)

    return cache[key]

The amount of memory used by an unbounded cache can grow. This isn't recurring trash. It is accessible memory that the program keeps.

So:

unreachable objects
    → garbage-collection problem

reachable but unnecessary objects
    → application memory-management problem

The collector cannot determine your application's intention.

Garbage Collection vs del

The Python del function is commonly searched for, but technically del is a statement, not a function.

For example:

items = [1, 2, 3]
alias = items

del items

This removes the name items.

It does not necessarily destroy the list because:

alias ──> list

still exists.

Only when the last relevant reference disappears can the object become unreachable. This distinction is essential when explaining object lifetime.

Garbage Collection vs Resource Cleanup

Garbage collection manages memory. It should not be used as a deterministic mechanism for closing external resources.

Examples include:

  • Files
  • Database connections
  • Sockets
  • Locks

Instead of:

file = open("data.txt")
content = file.read()
file = None

use:

with open("data.txt") as file:
    content = file.read()

When execution exits the with block, the context manager makes sure the resource is cleaned up.

So:

Garbage collection handles memory. Context managers handle deterministic resource cleanup.

This distinction matters regardless of whether you are studying garbage collection in C, garbage collection in Java, or memory management in Python—the mechanisms and guarantees differ between runtimes.

Weak References and Object Lifetime

A normal reference keeps an object reachable.

Sometimes you want to refer to an object without keeping it alive. A weak reference provides that relationship.

This can be useful for:

  • Caches
  • Registries
  • Observer systems
  • Non-owning relationships

The idea is:

strong reference
    → keeps object alive

weak reference
    → does not keep object alive

When the object's normal strong references disappear and it is otherwise eligible for reclamation, a weak reference does not prevent that lifecycle from completing.

Weak references are therefore helpful when creating systems in which an item should observe or refer to another without taking responsibility for its lifetime.

Practical Example: A Parent-Child Cycle

Consider:

class Parent:
    def __init__(self):
        self.children = []

class Child:
    def __init__(self, parent):
        self.parent = parent

parent = Parent()
child = Child(parent)

parent.children.append(child)

The graph becomes:

Parent
   │
  ▼
children ──> Child
                         │
                        └──> Parent

This is a valid relationship.

Now:

parent = None
child = None

If no other references reach the objects, the entire cycle becomes unreachable. The cycle can then be handled by CPython's cyclic garbage collector.

The important point is:

The cycle itself was never the problem. Losing reachability was.

Common Mistakes

  • Assuming every cycle is a memory leak: A reachable cycle is valid data. Only an unreachable cycle becomes cyclic garbage.
  • Assuming gc.collect() frees everything unused: It cannot collect objects that are still reachable.
  • Calling gc.collect() everywhere: Manual collection adds overhead and is usually unnecessary in normal application code.
  • Treating gc.get_count() as memory usage: Collector counters, not the overall amount of RAM used, are reported.
  • Assuming the three-generation model never changes: The generational concept is useful, but exact CPython implementation details can change between versions.
  • Using garbage collection for resource cleanup: Use context managers for files, sockets, locks, and similar resources.
  • Assuming del destroys an object: del removes a binding. Other references can keep the object alive.
  • Mistaking "garbage value" for garbage collection: Garbage is not always an unwanted value. Unreachable objects are the focus of garbage collection, not the logical use of a value.

Summary

In order to handle memory that reference counting alone is unable to recover, particularly unreachable reference cycles, Python's garbage collection works in tandem with reference counting. CPython's cyclic collector handles these objects very well by applying the principle of 'reachability' as well as the 'generational' approach. Even though the gc module offers some handy inspection and control tools, you shouldn't design your programs by letting collection times decide the flow. Instead, for guaranteed release of files, connections, and other resources, go for context managers. Grasping these two concepts will make Python's memory management and object lifetime easier to understand.

Frequently Asked Questions

Given that Python supports reference counting, why does it require garbage collection?

A cycle of objects that are not reachable can still reference each other via reference counting. Such reference counting alone is unable to reclaim those cycles, and garbage collection is still necessary.

What is a reference cycle?

A reference cycle occurs when references form a loop, such as: A → B → A It becomes cyclic garbage when the entire group is unreachable.

What is the difference between reachable and unreachable objects?

By tracking references from live program roots, a reachable object can still be accessed. It is not possible to reach an unreachable object in that manner.

What does gc.collect() do?

It explicitly requests a garbage-collection run. It is useful for debugging and controlled experiments but should not normally be required in application logic.

What does gc.get_count() do?

It returns the collector's current collection counters.

What does gc.get_threshold() do?

It returns the configured thresholds that influence automatic collection.

Can Python still have memory leaks?

Yes. If your program retains references to objects it no longer needs, those objects remain reachable and the garbage collector cannot reclaim them.

Does garbage collection close files automatically?

You should not depend on garbage collection for deterministic resource cleanup. Use a context manager such as with open(...).