Skip to main content

deferral

Release Build status codecov Commit activity License

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.

Metadata

Release files for deferral 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for deferral 0.1.0
File Size Uploaded
deferral-0.1.0.tar.gz 118.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for deferral 0.1.0
File Interpreter ABI Platform
deferral-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 125.0 kB

Release files / deferral-0.1.0.tar.gz

Download URL deferral-0.1.0.tar.gz
Size 118.4 kB
Tags Source
SHA-256 checksum
How to use checksums
aa29dca181d99d43ec2d07044474d185e60ad9635cfc2550ecf83a241ac33cee
BLAKE2b-256 checksum
How to use checksums
1fad4613ae46bf77ea8b7e7ad0bf84b0e289a111a4092f83c044f94d37fffad3
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / deferral-0.1.0-py3-none-any.whl

Download URL deferral-0.1.0-py3-none-any.whl
Size 6.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
46658dc2fc974f60f28e91845659c34eab5291569aecf9bd7b68460eb543a39b
BLAKE2b-256 checksum
How to use checksums
f16fc83251fd49abf2d831417599f5f702914239d5d22900c1a62a77e860b28f
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page