Skip to main content

Python's missing defer. Cleanup code where it belongs — next to the resource that needs cleaning up.

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

deferral-0.1.0.tar.gz (118.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

deferral-0.1.0-py3-none-any.whl (6.6 kB view details)

Uploaded Python 3

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

Hashes for deferral-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aa29dca181d99d43ec2d07044474d185e60ad9635cfc2550ecf83a241ac33cee
MD5 da0f4ce5f2d65dd69a5b69da095c4a88
BLAKE2b-256 1fad4613ae46bf77ea8b7e7ad0bf84b0e289a111a4092f83c044f94d37fffad3

See more details on using hashes here.

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

Hashes for deferral-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 46658dc2fc974f60f28e91845659c34eab5291569aecf9bd7b68460eb543a39b
MD5 38d607db03740055e2de866ebf7ccbc2
BLAKE2b-256 f16fc83251fd49abf2d831417599f5f702914239d5d22900c1a62a77e860b28f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page