Python Functions
Functions are the most important code-organization unit in Python. This article covers function definitions, parameter types, scope, closures, and modern type annotation usage.
Function Basics
Definition and Calling
def greet(name: str) -> str:
"""Return a greeting message."""
return f"Hello, {name}!"
print(greet("Alice")) # Hello, Alice!A function body consists of the def keyword, the function name, a parameter list, a colon, the function body, and an optional return statement.
- A function with no
return, or justreturn, returnsNone. - Use
"""..."""to write a docstring; view it withhelp(greet).
Multiple Return Values
def min_max(lst: list[int]) -> tuple[int, int]:
return min(lst), max(lst)
lo, hi = min_max([3, 1, 4, 1, 5, 9])
print(lo, hi) # 1 9Python actually returns a tuple; the call site unpacks it.
Parameter Types
Positional and Keyword Arguments
def register(name: str, age: int, city: str = "Beijing") -> None:
print(f"{name}, {age} years old, from {city}")
register("Alice", 25) # By position
register("Bob", age=30, city="Shanghai") # Keyword arguments can reorder
register("Carol", 22, "Guangzhou")Variable Positional Arguments *args
def add(*numbers: int) -> int:
return sum(numbers)
print(add(1, 2, 3, 4)) # 10
# Unpack a sequence at call time with *
nums = [1, 2, 3]
print(add(*nums)) # 6Variable Keyword Arguments **kwargs
def build_url(host: str, **params: str) -> str:
query = "&".join(f"{k}={v}" for k, v in params.items())
return f"{host}?{query}" if query else host
print(build_url("example.com", page="1", sort="name"))
# example.com?page=1&sort=name
# Unpack a dictionary at call time with **
options = {"page": "2", "size": "20"}
print(build_url("api.example.com", **options))Parameter Order Rules
Full parameter order: positional → default → *args → keyword-only → **kwargs
def func(pos1, pos2, default="x", *args, kw_only, **kwargs):
print(pos1, pos2, default, args, kw_only, kwargs)
func(1, 2, "y", 3, 4, kw_only="z", extra=99)
# 1 2 y (3, 4) z {'extra': 99}Positional-Only Parameters / (Python 3.8+)
def circle_area(radius: float, /, *, precision: int = 2) -> float:
import math
return round(math.pi * radius ** 2, precision)
circle_area(5) # radius can only be passed positionally
circle_area(5, precision=4) # precision can only be passed as a keyword
# circle_area(radius=5) # TypeErrorParameters to the left of / are positional-only; parameters to the right of * are keyword-only.
Scope (LEGB Rule)
Python resolves names in the order Local → Enclosing → Global → Built-in:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing
outer()
print(x) # globalglobal and nonlocal
counter = 0
def increment() -> None:
global counter # Declare intent to modify the global variable
counter += 1
def make_counter():
count = 0
def inc():
nonlocal count # Declare intent to modify the enclosing function's variable
count += 1
return count
return inc
c = make_counter()
print(c(), c(), c()) # 1 2 3Closures
A closure is an inner function that references variables (free variables) from an enclosing function. Even after the enclosing function has returned, the free variables remain alive:
def multiplier(factor: int):
def multiply(x: int) -> int:
return x * factor # factor is a free variable
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Inspect the free variable
print(double.__closure__[0].cell_contents) # 2Common uses: factory functions, deferred computation, stateful objects (a lightweight alternative to classes).
Functions as First-Class Objects
In Python, functions are first-class objects — they can be assigned to variables, passed as arguments, and returned as values:
def apply(func, values: list) -> list:
return [func(v) for v in values]
print(apply(str.upper, ["hello", "world"]))
# ['HELLO', 'WORLD']
# Store in a dictionary (strategy pattern)
ops = {
"add": lambda x, y: x + y,
"sub": lambda x, y: x - y,
"mul": lambda x, y: x * y,
}
print(ops["add"](3, 4)) # 7Recursion
A function that calls itself, directly or indirectly, is called recursive. Recursion must have a clear base case (the exit condition), or it will call forever until the stack overflows:
def factorial(n: int) -> int:
"""Compute n! Base case: n <= 1."""
if n <= 1:
return 1
return n * factorial(n - 1) # Recursive call, shrinking the problem each time
print(factorial(5)) # 120Fibonacci: Naive Recursion vs. Optimized
A naive recursive implementation is elegant but recomputes the same subproblems over and over, so it slows down noticeably even at moderate sizes:
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(30) # Already noticeably slow: fib(28) and friends get recomputed countless timesUse functools.lru_cache to memoize results and avoid recomputation:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_cached(n: int) -> int:
return n if n < 2 else fib_cached(n - 1) + fib_cached(n - 2)
print(fib_cached(100)) # 354224848179261915075, returns instantlyThe same problem can also be rewritten iteratively, which avoids recomputation entirely and isn’t bound by the recursion depth limit:
def fib_iter(n: int) -> int:
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return aRecursion Depth Limit
The Python interpreter protects itself with a limit on recursion depth; exceeding it raises RecursionError:
import sys
print(sys.getrecursionlimit()) # 1000 by default
sys.setrecursionlimit(3000) # Can be raised, but is still bounded by the OS stack size — don't raise it without limitType Annotations (Recommended)
Python 3.5+ supports type annotations. Combined with mypy or pyright, they enable static type checking:
from typing import Callable, Optional
from collections.abc import Sequence
def transform(
data: Sequence[int],
func: Callable[[int], int],
default: Optional[int] = None,
) -> list[int]:
"""Apply func to every element in the sequence."""
return [func(x) for x in data] if data else ([default] if default else [])
# Python 3.10+ shorthand for Union
def parse(value: int | str | None) -> str:
if value is None:
return "null"
return str(value)Commonly Used Built-in Functions
nums = [3, 1, 4, 1, 5, 9, 2, 6]
# Sorting (returns a new list)
print(sorted(nums)) # Ascending
print(sorted(nums, reverse=True)) # Descending
print(sorted(["banana", "apple"], key=len)) # Sort by length
# map / filter (return iterators)
doubled = list(map(lambda x: x * 2, nums))
evens = list(filter(lambda x: x % 2 == 0, nums))
# zip (parallel iteration over multiple sequences)
for a, b in zip([1, 2, 3], ["x", "y", "z"]):
print(a, b)
# enumerate (iterate with index)
for i, v in enumerate(nums, start=1):
print(f"{i}: {v}")
# any / all
print(any(x > 8 for x in nums)) # True
print(all(x > 0 for x in nums)) # True