backcall
Specify and enforce callback function signatures so third-party callbacks stay compatible as your API evolves.
Repository Health
Technical Analysis
backcall lets library authors declare the exact signature they expect from callback functions passed into their API, then automatically adapts callbacks that only implement a subset of those parameters. Instead of breaking every registered callback when a new parameter is added, backcall inspects each callback’s actual signature and wraps it so extra arguments are silently discarded.
Originally built by Thomas Kluyver for the IPython/Jupyter ecosystem, backcall is used internally by ipykernel and related IPython tooling to keep comm and event callback APIs backwards compatible as they grow new optional parameters over time.
What You Get
callback_prototypedecorator - Define a prototype function whose signature documents the exact positional and keyword arguments your API will pass to callbacks.- Automatic argument adaptation -
.adapt(callback)inspects a third-party callback’s signature and wraps it so only the arguments it declares are passed through. - Graceful handling of narrower callbacks - Callbacks that accept fewer parameters than the prototype are wrapped to silently discard the extras rather than raising a TypeError.
- Fail-fast on incompatible callbacks - If a callback expects parameters the prototype doesn’t provide,
.adapt()raisesTypeErrorat registration time instead of failing later at call time.
Common Use Cases
- Adding optional parameters to an event API - A library adds a new keyword argument to an existing callback signature without breaking every consumer’s already-registered handler.
- Building a plugin/callback registry - A framework accepts user-supplied callback functions and needs to normalize their signatures before storing and invoking them uniformly.
- Jupyter/IPython comm message handlers - IPython kernel internals use backcall to keep comm target and message callbacks compatible across versions with differing argument counts.
- Validating callback contracts early - Catch a mismatched callback signature when it’s registered, rather than surfacing a confusing TypeError deep inside an event loop.
Under The Hood
Architecture
backcall is a single flat module (backcall/backcall.py) re-exported through backcall/__init__.py, with a bundled compatibility shim (_signatures.py) providing a backport of inspect.signature/Parameter for interpreters that predate PEP 362. The callback_prototype decorator introspects the prototype function’s signature once, classifying each parameter as positional (no default) or keyword (has a default, or keyword-only), and attaches an adapt closure to the prototype. Calling adapt(callback) re-introspects the real callback’s signature, attempts a direct bind against the prototype’s expected arguments, and — if that fails — walks the callback’s parameters to build a trimmed wrapper via functools.wraps that discards unmatched arguments before delegating. There is no external state, no framework hooks, and no separation into layers beyond this single decorator/closure pair; since it is the library’s sole entry point, any change to the matching logic in adapt changes the behavior of every consumer.
Tech Stack
The library has zero runtime dependencies and relies entirely on the Python standard library’s inspect module, falling back to its own bundled _signatures.py backport on older interpreters. Packaging uses flit_core via a PEP 517/518 pyproject.toml (no setup.py). Tests are written for pytest. CI is configured only via a legacy .travis.yml, with no GitHub Actions workflow present.
Code Quality
A single test file (tests/test_callback_prototypes.py) exercises the core adapt logic across several branches — full positional+keyword matches, keyword-only parameters, callbacks with fewer parameters via defaults, and a no-argument callback — using plain assert statements in pytest style. Error handling is explicit and intentional: TypeError is raised both when a prototype declares *args/**kwargs (unsupported) and when a callback has unrecognised required parameters, rather than failing silently. There are no type annotations anywhere in the codebase (it predates widespread typing adoption) and no linter or formatter configuration is checked in.
What Makes It Unique
The core idea — declaring a callback’s contract as an ordinary function signature and using runtime introspection to structurally adapt real callbacks that implement only a subset of it — is a small, purpose-built solution to a common API-evolution problem: adding parameters to a callback contract without breaking already-registered handlers. It predates the mainstream adoption of typing.Protocol-style structural typing and solves the same class of problem with a lightweight runtime mechanism rather than static typing, extracted directly from real compatibility pain in the IPython/Jupyter callback surface.