Skip to Content
LanguageFunctions

Functions are the central abstraction. They are values. Pass them, return them, store them, compose them.

def

def add(a, b): return a + b print(add(3, 4))
Output · expected
7

Default arguments

def greet(name, greeting="Hello"): return f"{greeting}, {name}!" print(greet("world")) print(greet("world", "Hi"))
Output · expected
Hello, world! Hi, world!

Keyword arguments

def f(x, y, z): return x * 100 + y * 10 + z print(f(1, 2, 3)) print(f(x=1, z=3, y=2)) print(f(1, z=3, y=2))
Output · expected
123 123 123

Variadic: *args and **kwargs

def total(*nums): return sum(nums) print(total(1, 2, 3)) print(total(*[10, 20, 30]))
Output · expected
6 60
def opts(**kwargs): return sorted(kwargs.items()) print(opts(host="api", port=443))
Output · expected
[('host', 'api'), ('port', 443)]

Keyword-only parameters

A bare * marks the following parameters as keyword-only: they must be passed by name. A positional argument that would reach them is rejected (as is any positional beyond the declared parameters when there is no *args), raising TypeError.

def connect(host, *, port=80, secure=False): return f"{host}:{port} secure={secure}" print(connect("api")) print(connect("api", port=443, secure=True)) try: connect("api", 443) # positional can't fill a keyword-only param except TypeError: print("rejected")
Output · expected
api:80 secure=False api:443 secure=True rejected

Argument unpacking at the call site

def f(a, b, c): return a + b + c print(f(*[1, 2, 3])) print(f(*[1, 2], 3)) print(f(**{"a": 1, "b": 2, "c": 3})) print(f(1, **{"b": 2, "c": 3}))
Output · expected
6 6 6 6

lambda

Anonymous function. The body is a single expression.

double = lambda x: x * 2 print(double(21)) add = lambda a, b: a + b print(add(3, 4)) # With defaults greet = lambda name, msg="Hi": f"{msg}, {name}" print(greet("world"))
Output · expected
42 7 Hi, world

First-class functions

Functions are values: store, pass, return them.

ops = [abs, hex, str] print([f(-3) for f in ops])
Output · expected
[3, '-0x3', '-3']
# Functions as dict values; replaces switch/case handlers = { "add": lambda a, b: a + b, "mul": lambda a, b: a * b, "max": max, } print(handlers["add"](3, 4)) print(handlers["mul"](3, 4)) print(handlers["max"](3, 4))
Output · expected
7 12 4

Higher-order functions

Functions that take or return functions.

def apply(f, x): return f(x) print(apply(lambda n: n * n, 5)) print(apply(abs, -10))
Output · expected
25 10
# Returning a function def make_adder(n): return lambda x: x + n add5 = make_adder(5) add10 = make_adder(10) print(add5(3)) print(add10(3))
Output · expected
8 13

Closures

Functions capture their enclosing scope by reference.

def counter(): count = 0 def step(): nonlocal count count += 1 return count return step tick = counter() print(tick()) print(tick()) print(tick())
Output · expected
1 2 3
# Closures over loop variables; captured by reference def make_adders(n): return [lambda x, i=i: x + i for i in range(n)] add0, add1, add2 = make_adders(3) print(add0(10), add1(10), add2(10))
Output · expected
10 11 12

Currying

Partial application built from nested lambdas or closures.

add = lambda x: lambda y: x + y print(add(3)(4)) add3 = add(3) print(add3(10)) print(add3(100))
Output · expected
7 13 103
# Curry helper def curry(f): return lambda x: lambda y: f(x, y) cmul = curry(lambda a, b: a * b) double = cmul(2) triple = cmul(3) print(double(7), triple(7))
Output · expected
14 21

Function composition

def compose(*fns): def piped(x): for f in fns: x = f(x) return x return piped # Reads left-to-right: double, then square pipeline = compose(lambda n: n * 2, lambda n: n * n) print(pipeline(3)) # (3 * 2) ** 2 print([pipeline(x) for x in [1, 2, 3]])
Output · expected
36 [4, 16, 36]

Recursion

def factorial(n): if n < 2: return 1 return n * factorial(n - 1) print(factorial(10))
Output · expected
3628800
# Mutual recursion def is_even(n): return True if n == 0 else is_odd(n - 1) def is_odd(n): return False if n == 0 else is_even(n - 1) print(is_even(10), is_odd(10))
Output · expected
True False
Pure functions are memoized after two calls with the same arguments. The VM detects purity statically (no I/O, no mutation, no raise, no yield) and confirms it at runtime: any call that performs a side effect — including a builtin like `print` passed as a first-class value — marks the call impure and skips the cache, so memoization never drops an effect. Results live in a per-function template table. Naive recursion runs at memoized cost with no source changes.

Generators

yield-bearing functions produce sequences lazily. Pull with next() or iterate with for.

def squares(n): for i in range(n): yield i * i for x in squares(5): print(x)
Output · expected
0 1 4 9 16
# Materialize a generator def naturals(limit): n = 1 while n <= limit: yield n n += 1 print(list(naturals(5)))
Output · expected
[1, 2, 3, 4, 5]

yield from

Delegate to another generator.

def nums(): yield from range(3) yield from [10, 20] print(list(nums()))
Output · expected
[0, 1, 2, 10, 20]
Generators are one-way: producer to consumer. `gen.send(value)`, `gen.throw(exc)`, and `gen.close()` are not exposed. Bidirectional communication is a procedural pattern, inconsistent with the functional paradigm. For bidirectional flow, use the cooperative scheduler (`run` / `sleep` / `gather`). Pass values through arguments and return values.

Generator expressions

Generators inline:

print(sum(x * x for x in range(5))) print(max(i for i in [3, 1, 4, 1, 5]))
Output · expected
30 5

Decorators

A decorator wraps another callable. It applies to both functions and classes (see Classes):

def trace(f): def wrapped(*args): print(f"calling with {args}") return f(*args) return wrapped @trace def add(a, b): return a + b print(add(3, 4))
Output · expected
calling with (3, 4) 7

Stacked decorators apply bottom-up:

def double_result(f): return lambda *a: f(*a) * 2 def add_one(f): return lambda *a: f(*a) + 1 @double_result @add_one def base(x): return x # base(5) -> add_one -> 6 -> double_result -> 12 print(base(5))
Output · expected
12

Parameterised decorators are factories. A function takes the decorator args and returns the actual decorator. The wrapped function captures both scopes.

def repeat(n): def decorator(fn): def wrapped(x): for i in range(n): fn(x) return wrapped return decorator @repeat(3) def greet(name): print(f"hi {name}") greet("world")
Output · expected
hi world hi world hi world
Last updated on