Type Hints
Callable Types
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))
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))def add(a: int, b: int) -> int:
9def add(a2: int, b3: int) -> int:10 return a2 + b3op ← ⟨function mul B⟩
16op: Op = add17print("add:", op(2, 3))1819op→ ⟨function mul B⟩ = mul⟨function mul B⟩20print("mul:", op(2, 3))outputadd: 5def mul(a: int, b: int) -> int:
13def mul(a2: int, b3: int) -> int:14 return a2 * b3op ← <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:
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])))
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])))def sort_strings(items: list[str], key: KeyFn) -> list[str]:
pass 1 of 29def 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>)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']def sort_strings(items: list[str], key: KeyFn) -> list[str]:
pass 2 of 29def 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⟩>)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']
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])))def sort_strings(items: list[str], key: KeyFn) -> list[str]:
pass 1 of 29def 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>)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']def sort_strings(items: list[str], key: KeyFn) -> list[str]:
pass 2 of 29def 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⟩>)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']
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])))def sort_strings(items: list[str], key: KeyFn) -> list[str]:
pass 1 of 29def 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>)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']def sort_strings(items: list[str], key: KeyFn) -> list[str]:
pass 2 of 29def 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⟩>)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))
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))def trigger(event: str, handler: OnEvent) -> None:
pass 1 of 29def trigger(eventlogin: str, handler⟨function print_handler A⟩: OnEvent) -> None:10 handler(eventlogin)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: logintrigger("login", print_handler)
16trigger("login", print_handler⟨function print_handler A⟩)17trigger("logout", lambda e: print("lambda handled:", e))def trigger(event: str, handler: OnEvent) -> None:
pass 2 of 29def trigger(eventlogout: str, handler<function <lambda> at ⟨addr B⟩>: OnEvent) -> None:10 handler(eventlogout)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))
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)def make_multiplier(factor: int) -> UnaryInt:
pass 1 of 29def 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⟩>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)def make_multiplier(factor: int) -> UnaryInt:
pass 2 of 29def 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⟩>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))def mul(x: int) -> int:
pass 1 of 29def make_multiplier(factor: int) -> UnaryInt:10 def mul(x10: int) -> int:11 return x10 * factor2print("by2(10) =", by2(10))
18print("by2(10) =", by2(10))19print("by5(10) =", by5(10))outputby2(10) = 20def mul(x: int) -> int:
pass 2 of 29def make_multiplier(factor: int) -> UnaryInt:10 def mul(x10: int) -> int:11 return x10 * factor5print("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))
add10 = Adder(10)
13add10 = Adder(10)14print("add10(5) =", add10(5))self.n ← 10
6class Adder:7 def __init__(self⟨Adder A⟩, n10: int) -> None:8 self.n→ 10 = n10add10 ← ⟨Adder A⟩
13add10→ ⟨Adder A⟩ = Adder(10)14print("add10(5) =", add10(5))def __call__(self, x: int) -> int:
pass 1 of 210def __call__(self⟨Adder A⟩, x5: int) -> int:11 return x5 + self.n10Fn ← 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) = 15def __call__(self, x: int) -> int:
pass 2 of 210def __call__(self⟨Adder A⟩, x7: int) -> int:11 return x7 + self.n10print("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
Protocolinstead (beyond this page).
Exercise: practical.py
Create a validator registry using Callable types