External resources such as temporary files, native handles, helper sockets, and cache registrations must eventually be released. The primary path should use context managers and explicit close methods, but some libraries also need a safety net when an object is collected without being closed. weakref.finalize registers a callback that stays alive without keeping the observed object alive and runs when that object becomes unreachable.
This guide explains how to create finalizers, avoid accidental strong references, run cleanup early, use detach(), inspect alive, understand interpreter shutdown, test behavior, and decide why garbage collection should not be the main resource-management strategy.
Your first finalizer
import weakref
class Resource:
pass
def cleanup(name):
print("cleaning", name)
resource = Resource()
finalizer = weakref.finalize(resource, cleanup, "temporary")
While resource is reachable, the callback does not run. When it becomes unreachable, the finalizer may call cleanup("temporary").
The finalizer does not keep the object alive
The association uses a weak reference. However, callback arguments are strongly referenced. If one of them points back to the observed object, collection may be prevented.
# Avoid this:
weakref.finalize(resource, resource.close)
A bound method keeps its instance alive. Prefer an independent function receiving only the external state needed for cleanup:
weakref.finalize(resource, close_handle, resource.handle)
Idempotent cleanup
Cleanup should tolerate repeated requests or coordinate state so the resource is released once. A particular finalizer invokes its callback at most once, but the class may also expose an explicit close() path.
Running cleanup early
finalizer()
Calling the finalizer object runs the callback immediately when it is still alive and returns the callback result. Later calls do nothing.
The alive property
if finalizer.alive:
finalizer()
alive reports whether the callback remains registered and has not executed or been detached.
detach
details = finalizer.detach()
detach() deactivates the finalizer and, if it was alive, returns a tuple containing the object, callback, arguments, and keyword arguments. This supports transferring cleanup responsibility.
peek
details = finalizer.peek()
peek() inspects the registration without disabling it. The returned tuple may temporarily hold a strong reference to the object, so do not retain it unnecessarily.
Explicit close with a fallback
class TemporaryArtifact:
def __init__(self, path):
self.path = path
self._finalizer = weakref.finalize(
self,
remove_file,
path,
)
def close(self):
self._finalizer()
The explicit method provides deterministic release. The finalizer remains only a backup for forgotten cleanup.
Context managers remain preferable
with TemporaryArtifact(path) as resource:
use(resource)
A context manager guarantees cleanup when the block exits, including exceptions. Garbage-collection timing is not guaranteed across implementations and situations.
Why not rely on __del__
__del__ can complicate inheritance, cycles, exceptions, partially initialized objects, and interpreter shutdown. weakref.finalize separates the cleanup function and exposes explicit control through calling, alive, and detach().
Timing is not guaranteed
Do not make correctness depend on when the callback runs. References may remain in caches, closures, tracebacks, tasks, or threads. Python implementations may collect at different times.
Cyclic garbage
Finalizers are designed to cooperate more robustly with garbage collection, but callbacks should still avoid resurrecting objects or depending on the finalization order of a cycle.
Ordering between resources
When several objects become unreachable together, do not assume an implicit callback sequence. Model ownership explicitly so a manager closes dependent children before its own resource.
Interpreter shutdown
Live finalizers may run at process exit, generally in reverse creation order. The atexit property controls participation:
finalizer.atexit = False
During shutdown, modules and globals may already be partially torn down. Pass independent functions and concrete values to the callback rather than looking up global names late.
Exceptions in callbacks
Exceptions from automatic finalization cannot propagate normally to code that caused the collection. Keep callbacks small, catch expected failures, and log safely. Do not use a finalizer as the only place to persist critical data over a network.
Threads
The callback may run in a context different from the thread that created the resource. Avoid assumptions about thread-local state. If release must happen on a specific thread, explicitly schedule it there.
Asyncio resources
A finalizer is synchronous and cannot await. Do not invoke an async close coroutine directly. Provide async with and aclose(). At most, a finalizer may emit a warning or carefully signal a still-running event loop.
Temporary files
from pathlib import Path
import weakref
class Artifact:
def __init__(self, path: Path):
self.path = path
self._cleanup = weakref.finalize(
self,
Path.unlink,
path,
missing_ok=True,
)
def remove(self):
self._cleanup()
The callback function and path do not reference the instance. missing_ok=True makes removal idempotent if another path already deleted the file.
Native handles
When integrating with C libraries, keep only the handle value and safe release function in callback arguments. Confirm that the native library remains usable during interpreter shutdown.
Objects held in caches
If a cache holds a strong reference, the finalizer cannot run. Consider WeakValueDictionary or explicit eviction. Weak containers solve lifetime problems only when no other strong owner remains.
Testing finalization
Tests should primarily exercise explicit cleanup by calling close() or the finalizer and checking idempotency.
finalizer()
assert not finalizer.alive
finalizer() # no second call
A separate test can verify the garbage-collection fallback, but avoid making the suite depend entirely on implementation-specific collection timing.
Avoid closures that capture self
# Wrong: the closure captures self
weakref.finalize(self, lambda: release(self.handle))
Copy the handle into an independent local value and pass it to a top-level or static cleanup function.
Transferring ownership
When ownership moves to another object, detach the old finalizer and register a new one with the new owner. This prevents two independent components from releasing the same resource.
Observability
A fallback callback can increment a metric indicating the resource was not explicitly closed. Keep shutdown logging quiet and avoid including full objects or secret identifiers.
Performance
Every finalizer has registration and tracking cost. Do not create one for millions of tiny objects when a larger owner can manage resources in a single explicit batch.
Common mistakes
- Passing a bound method of the observed object: it keeps the instance alive.
- Using finalization as the primary cleanup path: prefer close and context managers.
- Trying to run a coroutine: finalizers are synchronous.
- Depending on callback ordering: model ownership explicitly.
- Looking up globals during shutdown: modules may be torn down.
- Ignoring idempotency: explicit and fallback paths must coordinate.
Complete native-resource example
import weakref
class NativeConnection:
def __init__(self, api):
handle = api.open()
self._api = api
self._handle = handle
self._finalizer = weakref.finalize(
self,
api.close,
handle,
)
@property
def closed(self):
return not self._finalizer.alive
def close(self):
self._finalizer()
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
self.close()
Normal use is deterministic through with. If a caller forgets, the finalizer still attempts to release the handle without retaining the instance.
Conclusion
weakref.finalize provides a controllable safety net for cleanup tied to object lifetime. It avoids several weaknesses of __del__, but it does not turn garbage collection into deterministic resource management.
The official Python weakref.finalize documentation defines the API. Use context managers as the primary path, never capture the observed object in the callback, and keep finalization small, idempotent, and shutdown-safe.







