I'm experimenting with asynchronous patterns in Python and was curious what happens if I define a __del__ method using async def in Python 3.12.
For example:
class MyClass:
async def __del__(self):
print("Async __del__ called")
Is this valid Python syntax?
Will the coroutine be awaited automatically when the object is garbage collected? If not awaited, is there any warning or silent failure?
Has this behavior changed in recent versions (like 3.11 → 3.12)?
Is there any recommended way to run async cleanup logic upon object deletion?
No - it simply won't run.
Python is a language which have a set of rules for how its classes behave. So, there are the dunder methods that are called by the language itself in certain circumstances. And then, the behavior for an async function: more formally a "coroutine function": When first called, it will immediately return a "coroutine" - when that coroutine is awaited, and only then, the code inside the function is itself executed.
Therefore, it is possible to create an async __getitem__ for example - upon doing myclass[key] in such a class, a coroutine, which is awaitable, is returned, so async_content = await myclass[key] will work.
But for __del__ , its return value is ignored: nothing would await a returned coroutine. You'd at most get a warning as the program ends that it was not awaited.
I'd suggest you to look for the asynchronous context manager protocol instead, use your object in an async with : block (it would be better than __del__ even for synchronous cases anyway) and put your asynchronous finalization code in the __aexit__ method instead.
(by the way, while it's nice to document this - for cases like this you could simply try to run your code and see the results: the learning experience is instant and much stronger than reading someone reasoning about it. )
update
It just occurred me that it is possible and valid to create a task within the __del__ method, and add it to a container outside the instance; the task should then run to completion. You could even have a "cleaner" infinite-running task to await any such tasks are awaited, but the tasks would run by themselves, nonetheless. The async with idea above is more deterministic, though, and ensure your parent task will only proceed once the child object cleanup in __aexit__ are executed. But if you don't need that:
import asyncio
# If you prefer, instead of top-level things in a module, both async_trashcan and async_gc
# could be defined in a class with a singleton behavior.
async_trashcan: asyncio.Queue # Just to indicate we should have this global resource
async def async_gc():
while True:
finalizer_task = await async_trashcan.get()
await finalizer_task
print(f"Executed {finalizer_task!r}")
class MyClass:
def __del__(self):
loop = asyncio.get_running_loop()
async def async_del():
print(f"Async __del__ called for {self}")
async_trashcan.put_nowait(loop.create_task(async_del(), name=f"finalizer task for {self}"))
async def main():
global async_trashcan
async_trashcan = asyncio.Queue()
gc_task = asyncio.create_task(async_gc())
m = MyClass()
del m
await asyncio.sleep(0.1)
print("exiting asyncio loop")
gc_task.cancel()
asyncio.run(main())
Is this valid Python syntax?
Strictly speaking, yes. __del__ is a hook method but it's not special-cased syntax, and since it's not expected to return anything there is no interaction with its return value after it's called.
Will the coroutine be awaited automatically when the object is garbage collected? If not awaited, is there any warning or silent failure?
There is a generic RuntimeWarning as with every other non-awaited coroutine:
RuntimeWarning: coroutine 'Bar.__del__' was never awaited
Is there any recommended way to run async cleanup logic upon object deletion?
Using __del__ is recommended against in the first place, you should use context managers, which exist in an async variant: https://docs.python.org/3/glossary.html#term-asynchronous-context-manager
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With