Functions
A function groups instructions that you can call with input values. Parameters name those inputs in the definition; arguments are the values supplied when calling it. The function returns a result or raises an exception. Below, clamp keeps a number within the interval from lower to upper: clamp(12, 0, 10) returns 10. With strict=True, an out-of-range value raises an exception instead. Annotations such as value: float describe expected input types, and -> float describes the expected return type; they do not enforce those types at runtime.
def clamp(value: float, lower: float, upper: float, *, strict: bool = False) -> float:
if lower > upper:
raise ValueError("lower must not exceed upper")
if strict and not lower <= value <= upper:
raise ValueError("value is outside the interval")
return min(max(value, lower), upper)
The * makes following parameters keyword-only, which is useful for behavioral
flags whose meaning would be unclear positionally.
Parameter kinds
Python supports positional-only parameters before /, positional-or-keyword
parameters, variadic positional *args, keyword-only parameters after *, and
variadic keyword **kwargs. Use the narrowest interface that communicates
intent; forwarding arbitrary arguments weakens discoverability.
Defaults are evaluated once
Default expressions run when the function is defined, not on every call. Avoid shared mutable defaults:
def collect(item: str, bucket: list[str] | None = None) -> list[str]:
if bucket is None:
bucket = []
bucket.append(item)
return bucket
Return and ownership
Reaching the end without return returns None. Returning multiple comma-
separated values constructs a tuple. Document whether a function mutates an
argument, returns a new object, performs I/O, or retains references.
Annotations and docstrings
Annotations communicate intended types to readers and tools; the interpreter does not generally enforce them. A concise docstring should explain behavior, important invariants, raised exceptions, and surprising side effects rather than repeat the signature.
Design rules
- Separate pure calculation from I/O when practical.
- Prefer explicit result values over printing inside reusable logic.
- Raise specific exceptions at the boundary where invalid state is recognized.
- Keep a function at one useful level of abstraction.
Local scope and shared arguments
Under Python's name resolution rules,
ordinary function code searches local names, enclosing function scopes, the module,
then built-ins. Assignment anywhere in a function normally makes that name local
throughout the function. Reading it before its local assignment raises
UnboundLocalError, even if a module variable has the same name. global targets
the module binding; nonlocal targets an existing enclosing function binding.
Neither is needed merely to read a name or mutate a referenced list.
def modify(items):
items.append(2) # mutate the caller's object
items = [99] # rebind only this local name
return items
numbers = [1]
assert modify(numbers) == [99]
assert numbers == [1, 2]
assert collect("a") == ["a"]
assert collect("b") == ["b"]
bucket = []
assert collect("c", bucket) is bucket
assert clamp(12, 0, 10) == 10
assert clamp(5, 0, 10, strict=True) == 5
The clamp example assumes finite, ordered numeric inputs; it is not a validator
for NaN or arbitrary objects. Calling clamp(5, 0, 10, True) raises TypeError
because strict is keyword-only. Missing required arguments, duplicate argument
bindings, and unexpected keywords also raise TypeError. At a call site, *values
unpacks positional arguments and **options unpacks a mapping of keyword arguments.
A def binds a function object without executing its body; this also allows
functions to be passed as values or returned from other functions.
In Python Tutor, call collect twice with its None default, then twice with the same explicit list. Follow the local parameter’s reference on each call to distinguish a fresh default-created list from intentionally shared caller state.