Python's missing defer. Cleanup code where it belongs, right next to the resource that needs cleaning up.
You crack open your Python project and the Star Wars opening crawl starts playing in your head, because the code is so deeply nested it looks center-aligned. Each try/finally block shunts your actual logic four more spaces to the right, until the function body is a thin column floating in the middle of the screen, and a long method genuinely resembles that yellow text drifting toward a galaxy far, far away... just like your sanity.
Does adding one more nesting level make your soul quietly leave the room? Do you need to wrap 200 lines of someone else's code in a try/finally for some small cleanup, but you dread inflating every git blame line with your name, for a change that adds zero logical value? Do you dread backporting bugfixes, because you know that the code shifted and indented so much that the merge conflict looks like a murder scene you have to now investigate?
Wouldn't it be preferable if some calls were deferrable?
# before: your soul, slowly departing
def provision(name):
conn = db.connect()
try:
lock = acquire_lock(name)
try:
tmp = tempfile.mkdtemp()
try:
result = do_work(conn, lock, tmp)
notify_success(name)
return result
finally:
shutil.rmtree(tmp, ignore_errors=True)
finally:
release_lock(lock)
finally:
conn.close()# after: your faith in humanity, gently returning
from deferral import defer_scope, defer, defer_on_success
@defer_scope
def provision(name):
conn = db.connect()
defer(conn.close)
lock = acquire_lock(name)
defer(release_lock, lock)
tmp = tempfile.mkdtemp()
defer(shutil.rmtree, tmp, ignore_errors=True)
result = do_work(conn, lock, tmp)
defer_on_success(notify_success, name)
return resultEach cleanup lives right next to the thing it cleans up. No extra indentation. No restructuring the entire function. No phantom git blame entries. And when the function exits - success or failure - everything runs in reverse order, just like Go's defer.
pip install deferralPython 3.7 – 3.14. No dependencies on 3.11+; uses the exceptiongroup backport on 3.7 – 3.10.
Wrap any function (sync or async) with @defer_scope to enable defer() calls inside it. Transparent: preserves the function's name, docstring, and signature.
from deferral import defer_scope, defer
@defer_scope
def my_function():
defer(print, "cleanup")
print("work")
# prints: work, then cleanupCan be used with arguments to configure the error handler for the whole scope:
from deferral import defer_scope, RAISE
@defer_scope(on_error=RAISE)
def my_function():
...Registers fn to run when the enclosing @defer_scope function exits, whether it succeeds or raises. Multiple calls run in LIFO order (last registered, first executed), just like finally blocks and Go's defer.
@defer_scope
def setup():
a = open_resource_a()
defer(a.close) # runs third
b = open_resource_b()
defer(b.close) # runs second
c = open_resource_c()
defer(c.close) # runs firstLike Zig's errdefer or D's scope(failure). The cleanup runs only if the function exits with an exception. Useful for rolling back partial state.
@defer_scope
def create_user(name):
user = db.insert_user(name)
defer_on_error(db.delete_user, user.id) # rollback on failure
send_welcome_email(user) # if this raises, the user is deleted
return user # if this returns, the user is keptThe mirror image, like D's scope(success). Runs only when the function returns cleanly.
@defer_scope
def complete_order(order_id):
result = process_payment(order_id)
defer_on_success(notify_warehouse, order_id) # only if payment succeeded
return resultAll three variants share a single LIFO queue and interleave naturally:
@defer_scope
def transfer_funds(src, dst, amount):
tx = db.begin()
defer(tx.close) # always close the transaction
debit(src, amount)
defer_on_error(credit, src, amount) # rollback debit on failure
credit(dst, amount)
defer_on_error(debit, dst, amount) # rollback credit on failure
defer_on_success(tx.commit) # commit only on full successFull API reference, error handling strategies, async and thread safety — claudiubelu.github.io/deferral.
MIT. See LICENSE.