Higher-order functions like sorting with a custom key, event callbacks, and plugin systems all need to accept functions as arguments. Callable types let you specify exactly what kind of function is expected - what arguments it takes and what it returns.

A callable is something you can call like a function: functions, lambdas, and objects with __call__.

Callable Basics

Use typing.Callable to describe "a function that takes these arguments and returns this type".

callable_basics.py
Replay: real traced execution (multi-file project)
# Callable basics

from typing import Callable

Op = Callable[[int, int], int]

# Functions match Callable types

def add(a: int, b: int) -> int:
    return a + b


def mul(a: int, b: int) -> int:
    return a * b

op: Op = add
print("add:", op(2, 3))

op = mul
print("mul:", op(2, 3))

# Lambda also matches
op = lambda a, b: a - b
print("sub:", op(7, 2))

  1. Op ← typing.Callable[[int, int], int], op ← ⟨function add A⟩

    5Op→ typing.Callable[[int, int], int] = Callable[[int, int], int]typing.Callable[[int, int], int]67# Functions match Callable types89def add(a: int, b: int) -> int:10    return a + b111213def mul(a: int, b: int) -> int:14    return a * b1516op→ ⟨function add A⟩: Op = add⟨function add A⟩17print("add:", op(2, 3))
  2. def add(a: int, b: int) -> int:

    9def add(a2: int, b3: int) -> int:10    return a2 + b3
  3. op ← ⟨function mul B⟩

    16op: Op = add17print("add:", op(2, 3))1819op→ ⟨function mul B⟩ = mul⟨function mul B⟩20print("mul:", op(2, 3))
    outputadd: 5
  4. def mul(a: int, b: int) -> int:

    13def mul(a2: int, b3: int) -> int:14    return a2 * b3
  5. op ← <function <lambda> at ⟨addr C⟩>

    19op = mul20print("mul:", op(2, 3))2122# Lambda also matches23op→ <function <lambda> at ⟨addr C⟩> = lambda a, b: a - b24print("sub:", op(7, 2))
    outputmul: 6
    sub: 5
Callable - a type annotation `Callable[[ArgTypes...], ReturnType]` describing a function's signature

Key Functions

Common use case: sorting with a custom key:

items
key_functions.py
Replay: real traced execution (multi-file project)
# Callable key functions

from typing import Callable

KeyFn = Callable[[str], int]

# A custom sort using a key function

def sort_strings(items: list[str], key: KeyFn) -> list[str]:
    return sorted(items, key=key)

items = ["apple", "pear", "banana"]

print("by length:", sort_strings(items, key=len))
print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))

# Callable key functions

from typing import Callable

KeyFn = Callable[[str], int]

# A custom sort using a key function

def sort_strings(items: list[str], key: KeyFn) -> list[str]:
    return sorted(items, key=key)

items = ["kiwi", "watermelon", "fig"]

print("by length:", sort_strings(items, key=len))
print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))

# Callable key functions

from typing import Callable

KeyFn = Callable[[str], int]

# A custom sort using a key function

def sort_strings(items: list[str], key: KeyFn) -> list[str]:
    return sorted(items, key=key)

items = ["java", "python", "go"]

print("by length:", sort_strings(items, key=len))
print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))

  1. KeyFn ← typing.Callable[[str], int], items ← ['apple', 'pear', 'banana']

    5KeyFn→ typing.Callable[[str], int] = Callable[[str], int]typing.Callable[[str], int]67# A custom sort using a key function89def sort_strings(items: list[str], key: KeyFn) -> list[str]:10    return sorted(items, key=key)1112items→ ['apple', 'pear', 'banana'] = ["apple", "pear", "banana"]13#@items=["kiwi", "watermelon", "fig"], ["java", "python", "go"]1415print("by length:", sort_strings(items['apple', 'pear', 'banana'], key=len))16print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))
  2. def sort_strings(items: list[str], key: KeyFn) -> list[str]:

    pass 1 of 2
    9def sort_strings(items['apple', 'pear', 'banana']: list[str], key<built-in function len>: KeyFn) -> list[str]:10    return sorted(items['apple', 'pear', 'banana'], key=key<built-in function len>)
  3. print("by length:", sort_strings(items, key=len))

    15print("by length:", sort_strings(items['apple', 'pear', 'banana'], key=len))16print("by last char:", sort_strings(items['apple', 'pear', 'banana'], key=lambda s: ord(s[-1])))
    outputby length: ['pear', 'apple', 'banana']
  4. def sort_strings(items: list[str], key: KeyFn) -> list[str]:

    pass 2 of 2
    9def sort_strings(items['apple', 'pear', 'banana']: list[str], key<function <lambda> at ⟨addr A⟩>: KeyFn) -> list[str]:10    return sorted(items['apple', 'pear', 'banana'], key=key<function <lambda> at ⟨addr A⟩>)
  5. print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))

    15print("by length:", sort_strings(items, key=len))16print("by last char:", sort_strings(items['apple', 'pear', 'banana'], key=lambda s: ord(s[-1])))
    outputby last char: ['banana', 'apple', 'pear']
  1. KeyFn ← typing.Callable[[str], int], items ← ['kiwi', 'watermelon', 'fig']

    5KeyFn→ typing.Callable[[str], int] = Callable[[str], int]typing.Callable[[str], int]67# A custom sort using a key function89def sort_strings(items: list[str], key: KeyFn) -> list[str]:10    return sorted(items, key=key)1112items→ ['kiwi', 'watermelon', 'fig'] = ["kiwi", "watermelon", "fig"]1314print("by length:", sort_strings(items['kiwi', 'watermelon', 'fig'], key=len))15print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))
  2. def sort_strings(items: list[str], key: KeyFn) -> list[str]:

    pass 1 of 2
    9def sort_strings(items['kiwi', 'watermelon', 'fig']: list[str], key<built-in function len>: KeyFn) -> list[str]:10    return sorted(items['kiwi', 'watermelon', 'fig'], key=key<built-in function len>)
  3. print("by length:", sort_strings(items, key=len))

    14print("by length:", sort_strings(items['kiwi', 'watermelon', 'fig'], key=len))15print("by last char:", sort_strings(items['kiwi', 'watermelon', 'fig'], key=lambda s: ord(s[-1])))
    outputby length: ['fig', 'kiwi', 'watermelon']
  4. def sort_strings(items: list[str], key: KeyFn) -> list[str]:

    pass 2 of 2
    9def sort_strings(items['kiwi', 'watermelon', 'fig']: list[str], key<function <lambda> at ⟨addr A⟩>: KeyFn) -> list[str]:10    return sorted(items['kiwi', 'watermelon', 'fig'], key=key<function <lambda> at ⟨addr A⟩>)
  5. print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))

    14print("by length:", sort_strings(items, key=len))15print("by last char:", sort_strings(items['kiwi', 'watermelon', 'fig'], key=lambda s: ord(s[-1])))
    outputby last char: ['fig', 'kiwi', 'watermelon']
  1. KeyFn ← typing.Callable[[str], int], items ← ['java', 'python', 'go']

    5KeyFn→ typing.Callable[[str], int] = Callable[[str], int]typing.Callable[[str], int]67# A custom sort using a key function89def sort_strings(items: list[str], key: KeyFn) -> list[str]:10    return sorted(items, key=key)1112items→ ['java', 'python', 'go'] = ["java", "python", "go"]1314print("by length:", sort_strings(items['java', 'python', 'go'], key=len))15print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))
  2. def sort_strings(items: list[str], key: KeyFn) -> list[str]:

    pass 1 of 2
    9def sort_strings(items['java', 'python', 'go']: list[str], key<built-in function len>: KeyFn) -> list[str]:10    return sorted(items['java', 'python', 'go'], key=key<built-in function len>)
  3. print("by length:", sort_strings(items, key=len))

    14print("by length:", sort_strings(items['java', 'python', 'go'], key=len))15print("by last char:", sort_strings(items['java', 'python', 'go'], key=lambda s: ord(s[-1])))
    outputby length: ['go', 'java', 'python']
  4. def sort_strings(items: list[str], key: KeyFn) -> list[str]:

    pass 2 of 2
    9def sort_strings(items['java', 'python', 'go']: list[str], key<function <lambda> at ⟨addr A⟩>: KeyFn) -> list[str]:10    return sorted(items['java', 'python', 'go'], key=key<function <lambda> at ⟨addr A⟩>)
  5. print("by last char:", sort_strings(items, key=lambda s: ord(s[-1])))

    14print("by length:", sort_strings(items, key=len))15print("by last char:", sort_strings(items['java', 'python', 'go'], key=lambda s: ord(s[-1])))
    outputby last char: ['java', 'python', 'go']

Callbacks and Event Handlers

callbacks.py
Replay: real traced execution (multi-file project)
# Callbacks

from typing import Callable

OnEvent = Callable[[str], None]

# Register/trigger

def trigger(event: str, handler: OnEvent) -> None:
    handler(event)


def print_handler(event: str) -> None:
    print("handled:", event)

trigger("login", print_handler)
trigger("logout", lambda e: print("lambda handled:", e))

  1. OnEvent ← typing.Callable[[str], NoneType]

    5OnEvent→ typing.Callable[[str], NoneType] = Callable[[str], None]typing.Callable[[str], NoneType]67# Register/trigger89def trigger(event: str, handler: OnEvent) -> None:10    handler(event)111213def print_handler(event: str) -> None:14    print("handled:", event)1516trigger("login", print_handler⟨function print_handler A⟩)17trigger("logout", lambda e: print("lambda handled:", e))
  2. def trigger(event: str, handler: OnEvent) -> None:

    pass 1 of 2
    9def trigger(eventlogin: str, handler⟨function print_handler A⟩: OnEvent) -> None:10    handler(eventlogin)
  3. def print_handler(event: str) -> None:

    9def trigger(event: str, handler: OnEvent) -> None:10    handler(eventlogin)111213def print_handler(eventlogin: str) -> None:14    print("handled:", eventlogin)
    outputhandled: login
  4. trigger("login", print_handler)

    16trigger("login", print_handler⟨function print_handler A⟩)17trigger("logout", lambda e: print("lambda handled:", e))
  5. def trigger(event: str, handler: OnEvent) -> None:

    pass 2 of 2
    9def trigger(eventlogout: str, handler<function <lambda> at ⟨addr B⟩>: OnEvent) -> None:10    handler(eventlogout)
  6. trigger("logout", lambda e: print("lambda handled:", e))

    16trigger("login", print_handler)17trigger("logout", lambda e: print("lambda handled:", e))

Higher-Order Functions

higher_order.py
Replay: real traced execution (multi-file project)
# Higher-order functions

from typing import Callable

UnaryInt = Callable[[int], int]

# Factory

def make_multiplier(factor: int) -> UnaryInt:
    def mul(x: int) -> int:
        return x * factor

    return mul

by2 = make_multiplier(2)
by5 = make_multiplier(5)

print("by2(10) =", by2(10))
print("by5(10) =", by5(10))

  1. UnaryInt ← typing.Callable[[int], int]

    5UnaryInt→ typing.Callable[[int], int] = Callable[[int], int]typing.Callable[[int], int]67# Factory89def make_multiplier(factor: int) -> UnaryInt:10    def mul(x: int) -> int:11        return x * factor1213    return mul1415by2 = make_multiplier(2)16by5 = make_multiplier(5)
  2. def make_multiplier(factor: int) -> UnaryInt:

    pass 1 of 2
    9def make_multiplier(factor2: int) -> UnaryInt:10    def mul(x: int) -> int:11        return x * factor1213    return mul<function make_multiplier.<locals>.mul at ⟨addr A⟩>
  3. by2 ← <function make_multiplier.<locals>.mul at ⟨addr A⟩>

    15by2→ <function make_multiplier.<locals>.mul at ⟨addr A⟩> = make_multiplier(2)16by5 = make_multiplier(5)
  4. def make_multiplier(factor: int) -> UnaryInt:

    pass 2 of 2
    9def make_multiplier(factor5: int) -> UnaryInt:10    def mul(x: int) -> int:11        return x * factor1213    return mul<function make_multiplier.<locals>.mul at ⟨addr B⟩>
  5. by5 ← <function make_multiplier.<locals>.mul at ⟨addr B⟩>

    15by2 = make_multiplier(2)16by5→ <function make_multiplier.<locals>.mul at ⟨addr B⟩> = make_multiplier(5)1718print("by2(10) =", by2(10))19print("by5(10) =", by5(10))
  6. def mul(x: int) -> int:

    pass 1 of 2
    9def make_multiplier(factor: int) -> UnaryInt:10    def mul(x10: int) -> int:11        return x10 * factor2
  7. print("by2(10) =", by2(10))

    18print("by2(10) =", by2(10))19print("by5(10) =", by5(10))
    outputby2(10) = 20
  8. def mul(x: int) -> int:

    pass 2 of 2
    9def make_multiplier(factor: int) -> UnaryInt:10    def mul(x10: int) -> int:11        return x10 * factor5
  9. print("by5(10) =", by5(10))

    18print("by2(10) =", by2(10))19print("by5(10) =", by5(10))
    outputby5(10) = 50
higher-order function - a function that takes another function as an argument or returns a function

Callable Objects

Objects with __call__ are also callable:

callable_objects.py
Replay: real traced execution (multi-file project)
# Callable objects

from typing import Callable

# A callable class
class Adder:
    def __init__(self, n: int) -> None:
        self.n = n

    def __call__(self, x: int) -> int:
        return x + self.n

add10 = Adder(10)
print("add10(5) =", add10(5))

# It can be treated like a Callable[[int], int]
Fn = Callable[[int], int]
f: Fn = add10
print("f(7) =", f(7))

  1. add10 = Adder(10)

    13add10 = Adder(10)14print("add10(5) =", add10(5))
  2. self.n ← 10

    6class Adder:7    def __init__(self⟨Adder A⟩, n10: int) -> None:8        self.n→ 10 = n10
  3. add10 ← ⟨Adder A⟩

    13add10→ ⟨Adder A⟩ = Adder(10)14print("add10(5) =", add10(5))
  4. def __call__(self, x: int) -> int:

    pass 1 of 2
    10def __call__(self⟨Adder A⟩, x5: int) -> int:11    return x5 + self.n10
  5. Fn ← typing.Callable[[int], int], f ← ⟨Adder A⟩

    13add10 = Adder(10)14print("add10(5) =", add10(5))1516# It can be treated like a Callable[[int], int]17Fn→ typing.Callable[[int], int] = Callable[[int], int]typing.Callable[[int], int]18f→ ⟨Adder A⟩: Fn = add10⟨Adder A⟩19print("f(7) =", f(7))
    outputadd10(5) = 15
  6. def __call__(self, x: int) -> int:

    pass 2 of 2
    10def __call__(self⟨Adder A⟩, x7: int) -> int:11    return x7 + self.n10
  7. print("f(7) =", f(7))

    18f: Fn = add1019print("f(7) =", f(7))
    outputf(7) = 17

When to Use Callable

  • Callbacks / event handlers
  • Customizing behavior (strategy pattern)
  • Mapping / filtering utilities
  • Dependency injection in small scripts If you need many overloads, consider Protocol instead (beyond this page).

Exercise: practical.py

Create a validator registry using Callable types