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 ExitStackfrom dataclasses import dataclassfrom pathlib import Path@dataclass(frozen=True, slots=True)class ExportTarget: path: Path text: strdef 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 dataclassfrom weakref import finalize@dataclassclass CacheFile: name: strdef announce(name: str) -> None: print(f"finalizing {name}")cache = CacheFile("batch.tmp")cleanup = finalize(cache, announce, cache.name)print(cleanup.alive)del cacheprint("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 dataclassfrom pathlib import Path@dataclass(frozen=True, slots=True)class Report: path: Path body: strdef 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.