Python's missing defer. Cleanup code where it belongs — next to the resource that needs cleaning up.
Project description
deferral
Python's missing defer. Cleanup code where it belongs, right next to the resource that needs cleaning up.
Has this ever happened to you?
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 result
Each 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.
Installation
pip install deferral
Python 3.7 – 3.14. No dependencies on 3.11+; uses the exceptiongroup backport on 3.7 – 3.10.
Core API
@defer_scope - the decorator
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 cleanup
Can 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():
...
defer(fn) - always runs
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 first
defer_on_error(fn) - runs only on failure
Like 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 kept
defer_on_success(fn) - runs only on success
The 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 result
Mixing them all
All 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 success
Further reading
Full API reference, error handling strategies, async and thread safety — claudiubelu.github.io/deferral.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deferral-0.1.0.tar.gz.
File metadata
- Download URL: deferral-0.1.0.tar.gz
- Upload date:
- Size: 118.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa29dca181d99d43ec2d07044474d185e60ad9635cfc2550ecf83a241ac33cee
|
|
| MD5 |
da0f4ce5f2d65dd69a5b69da095c4a88
|
|
| BLAKE2b-256 |
1fad4613ae46bf77ea8b7e7ad0bf84b0e289a111a4092f83c044f94d37fffad3
|
File details
Details for the file deferral-0.1.0-py3-none-any.whl.
File metadata
- Download URL: deferral-0.1.0-py3-none-any.whl
- Upload date:
- Size: 6.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46658dc2fc974f60f28e91845659c34eab5291569aecf9bd7b68460eb543a39b
|
|
| MD5 |
38d607db03740055e2de866ebf7ccbc2
|
|
| BLAKE2b-256 |
f16fc83251fd49abf2d831417599f5f702914239d5d22900c1a62a77e860b28f
|