del, references, and garbage collection

Deleting a name is not deleting an object; resource cleanup needs with

`del` in Python deletes names, not objects. The object survives as long as any reference to it remains. CPython uses reference counting as its primary memory management. Every `PyObject*` has an `ob_refcnt` field, incremented on reference acquisition and decremented on release. When the count reaches zero, the object is deallocated immediately. Cyclic garbage collection handles the remaining case: objects that reference each other but are no longer reachable from any root. Non-container objects like strings and integers are never tracked because they cannot form cycles. The `gc` module exposes collection control and debugging. `__del__` finalizers are unreliable for deterministic cleanup. Context managers via `with` are the correct pattern for resource management. <a href="/memory-tuples-lists">See how reference counting affects tuple and list memory</a>. <a href="/language-identity-equality">Understand how aliasing creates additional references</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Cleanup belongs in `with`, not `__del__`.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
2.8del, references, and garbage collection

Deleting a name is not deleting an object; resource cleanup needs with

del removes a reference binding or container slot. It does not mean "destroy this object now", and it is not a resource-management API.

Core answer

Use with and explicit close operations for deterministic cleanup. Use del to remove a name or item when that is what the code means. Treat object finalization as a backstop, not as your transaction boundary.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from contextlib import ExitStack
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True, slots=True)
class ExportTarget:
path: Path
text: str
def write_targets(targets: list[ExportTarget]) -> None:
with ExitStack() as stack:
files = [stack.enter_context(target.path.open("w", encoding="utf-8")) for target in targets]
for target, handle in zip(targets, files, strict=True):
handle.write(target.text)
write_targets([ExportTarget(Path("/tmp/python-in-depth-gc.txt"), "closed by context\n")])

Why this design exists

Python separates object reachability from external resource lifetime. Reference semantics let objects be shared freely; context managers provide a visible lexical cleanup boundary for files, locks, sockets, and database sessions.

CPython's eager reference counting can make destruction appear deterministic in small examples. That appearance is not the language guarantee across implementations, cycles, or finalizers.

Mechanics and CPython internals

CPython increments and decrements references on PyObject instances and deallocates many unreachable objects immediately when a reference count reaches zero. Cyclic garbage collection supplements that model for groups of container objects that keep each other alive. __del__ changes finalization risk and debugging cost; weakref.finalize can register cleanup without binding the callback to the object's method dispatch.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
from weakref import finalize
@dataclass
class CacheFile:
name: str
def announce(name: str) -> None:
print(f"finalizing {name}")
cache = CacheFile("batch.tmp")
cleanup = finalize(cache, announce, cache.name)
print(cleanup.alive)
del cache
print("binding removed; callback timing follows reachability")

Complexity and tradeoffs

del name is a binding operation. Deallocation work can cascade through object graphs and container references; cyclic GC adds collection passes whose cost depends on tracked objects and cycle shape. Deterministic cleanup moves operational risk out of that runtime timing and into a visible control-flow boundary.

Idiomatic patterns and refactoring

Refactor finalizer-dependent resource code into context-manager ownership.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True, slots=True)
class Report:
path: Path
body: str
def write_bad(report: Report) -> None:
handle = report.path.open("w", encoding="utf-8")
handle.write(report.body)
def write_report(report: Report) -> None:
with report.path.open("w", encoding="utf-8") as handle:
handle.write(report.body)
sample = Report(Path("/tmp/python-in-depth-report.txt"), "deterministic cleanup\n")
write_bad(sample)
write_report(sample)

Common mistakes and edge cases

Do not assume del obj destroys an object still referenced elsewhere. Do not rely on __del__ ordering during interpreter shutdown. Do not use GC timing as a substitute for releasing finite external resources under load.

When to use / When NOT to use

Use del for names, indexes, attributes, and large references you deliberately want to drop from a live scope. Use context managers for resource lifetimes.

Do not reach for gc.collect() in normal application logic to make cleanup "happen"; fix ownership and lifetime boundaries first.

Further reading

  • Official docs: del statements
  • Official docs: garbage collection
  • Official docs: weakref.finalize
  • PEP 442: safe object finalization
  • CPython source: cyclic GC
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Cleanup belongs in `with`, not `__del__`.

RELATED GUIDES
NEXT CHECKS
Contribute