Home/Python/Memory and garbage collection

Memory and garbage collection

References, cyclic garbage, weak caches, and memory diagnosis

Reachability

Names, containers, attributes, closures, caches, and stack frames can hold strong references. An object remains reachable while those paths lead to it. del name removes one binding; other owners may retain the object.

CPython combines reference counting with a cyclic collector. Objects can remain in an unreachable cycle after outside owners disappear. The collector can reclaim such cycles, but exact destruction timing is not a language guarantee:

Python
import gc
import weakref

class Document:
    pass

item = Document()
item.link = item
observer = weakref.ref(item)
retained = [item]
del item
print(observer() is retained[0])  # True
retained.clear()
gc.collect()
print(observer() is None)  # True

The list first retains the document. Clearing it leaves only the self-cycle; the explicit collection request handles that controlled unreachable cycle in CPython. The weak observer does not keep it alive.

Weak references

A weak reference observes an object without owning it. Retrieve the referent once and hold that local strong reference during use. Checking once and dereferencing again can race with loss of the last owner.

Ordinary user-defined instances generally support weak references; ordinary lists and dictionaries do not directly. A weak-value index makes objects discoverable while another owner retains them:

Python
import gc
import weakref

class Artifact:
    pass

artifact = Artifact()
cache = weakref.WeakValueDictionary({"latest": artifact})
owners = [artifact]
del artifact
print("latest" in cache)  # True
owners.clear()
gc.collect()
print("latest" in cache)  # False

Use weak indexing when misses can reload or recreate values. Use a bounded strong cache with eviction when retention is required. Check for other strong owners in callbacks, keys, and auxiliary containers.

Diagnosing growth

  • A cycle is not automatically a leak. An unbounded reachable cache can retain memory without any cycle; gc.collect() cannot remove reachable data.
  • Reclaimed objects need not cause an immediate drop in process memory. Python's allocator can retain memory for reuse.
  • Look for retained paths through caches, callbacks, closures, suspended generators, and stored tracebacks. Entry count alone does not measure payload size.
  • Use context managers or explicit close() for resources; finalizers are not a dependable lifetime boundary.

Where supported, start tracemalloc before the workload, take snapshots, and compare with after.compare_to(before, "lineno"). Warm up imports and caches before measuring steady state. Traces cover Python allocations, not all native or OS memory; sys.getsizeof does not recursively total an object graph. Exact sizes vary by runtime, and some browser builds omit allocation tracing.

Put it into practice

Implement the exercise in a full-screen workspace, then run its tests.

Open practice