Version 1

My software has a callback API, to call functions with one argument:

In [1]:
tone_detected_callbacks = []
def on_tone_detected(callback):
    tone_detected_callbacks.append(callback)

def tone_detected(pitch):
    for callback in tone_detected_callbacks:
        callback(pitch)

Sara writes a plugin which provides a callback:

In [2]:
def tone_callback_a(pitch):
    print("Tone detected at %f Hz" % pitch)

on_tone_detected(tone_callback_a)

And there was much rejoicing.

In [3]:
tone_detected(227.5)
Tone detected at 227.500000 Hz

Version 2

The software becomes more complex, and it can provide more information to callbacks:

In [4]:
def tone_detected(pitch, duration):
    for callback in tone_detected_callbacks:
        callback(pitch, duration)

But Sara's plugin hasn't been updated yet, so it doesn't expect the extra parameter.

In [5]:
tone_detected(227.5, 3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-fc61d95b6669> in <module>()
----> 1 tone_detected(227.5, 3)

<ipython-input-4-dc14bf1e421f> in tone_detected(pitch, duration)
      1 def tone_detected(pitch, duration):
      2     for callback in tone_detected_callbacks:
----> 3         callback(pitch, duration)

TypeError: tone_callback_a() takes 1 positional argument but 2 were given

Using backcall

backcall is a library to solve this problem, so you can extend callback APIs in a backwards compatible way.

In [6]:
from backcall import callback_prototype

# A callback prototype specifies what parameters we're going to pass

@callback_prototype
def tone_detected_cb(pitch, duration):
    pass

tone_detected_callbacks = []
def on_tone_detected(callback):
    # This inspects callback, and wraps it in a function that will discard extra arguments
    adapted = tone_detected_cb.adapt(callback)
    tone_detected_callbacks.append(adapted)

def tone_detected(pitch, duration):
    for callback in tone_detected_callbacks:
        callback(pitch, duration)

Registering the callback looks just the same as before - callback providers don't need to do anything special.

In [7]:
on_tone_detected(tone_callback_a)

Now the extra parameter is discarded, and Sara's plugin gets only the information it expects.

In [8]:
tone_detected(227.5, 3)
Tone detected at 227.500000 Hz

Plus you've got an introspectable reference for the expected callback signature:

In [9]:
help(tone_detected_cb)
Help on function tone_detected_cb in module __main__:

tone_detected_cb(pitch, duration)