Basic

Handing work over

A callback is a function you pass to something else so it can call it when the work is done. The call happens later, in a place you do not control.

What you will see

8

About 2 min of reading

Try It

Check what a callback receives.

1

Value doubled.

def run(work, done): done(work(3)) run(lambda x: x * 2, lambda v: print(v))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
6
2

Name read.

name = "ada" def greet(other): print(name, other) greet("grace")

Before you run it, say out loud what it will print. Then open the answer.

What it prints
ada grace
3

Error branch.

def on_done(err, value): if err: print("failed", err) else: print("ok", value) on_done("late", None)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
failed late
4

Pipeline run.

steps = [lambda v: v + 1, lambda v: v * 2] value = 3 for s in steps: value = s(value) print(value)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
8

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

You hand a function over, the runner keeps it, the runner calls it when the value is ready, and your body runs in the runners scope.
1 hand the function over
↓
2 the runner keeps it
↓
3 the runner calls it
↓
4 your body runs
Why this works
You do not call it

The code that received your function decides when and how often it runs. That is why a callback body must be safe to run more than once.

Later is not never

A callback can be dropped instead of called. Any path that waits on one needs a matching path for the case where it never arrives.

Examples

Pass a function

def later(value):
    print("done", value)

def run(work, done):
    result = work(3)
    done(result)

run(lambda x: x * 2, later)

The runner does not know what later is. It only knows to call it once a value exists.

A callback that closes

name = "ada"
def greet(other):
    print(name, other)

greet("grace")

The callback reads a name from around it, so it can use data it was never handed.

One slot for two outcomes

def on_done(err, value):
    if err:
        print("failed", err)
    else:
        print("ok", value)

on_done(None, 7)
on_done("late", None)

One slot carries either the error or the value, so every callback body has to ask which one arrived.

Nested callbacks

def a(x):
    def b(y):
        print(x + y)
    b(2)

a(5)

Nesting grows down and to the right. Two levels reads fine, five levels is a shape worth fixing.

A list of callbacks

steps = [lambda v: v + 1, lambda v: v * 2]
value = 3
for s in steps:
    value = s(value)
print(value)

A list of callbacks is a pipeline. You can lengthen it without touching the loop that runs it.

Count the calls

calls = 0
def once():
    global calls
    calls += 1
    return calls

once()
print(once())

A callback that runs twice doubles the work. A counter is the cheapest way to see that.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

Next Lesson
Waiting without blocking
Waiting without blocking →