Chapter 07 of 24 | Python

Functions and program structure

A function packages a responsibility behind a name. Parameters provide inputs, the return value provides an output, and a well-designed function keeps unrelated work outside its boundary.

def rectangle_area(width: float, height: float) -> float:
    """Return the area of a rectangle."""
    if width < 0 or height < 0:
        raise ValueError("dimensions must be non-negative")
    return width * height

area = rectangle_area(4, 2.5)

The return statement sends a result to the caller. A function without an explicit return produces None. A docstring is a string literal at the start of a function body and can document its purpose, parameters, return value, and exceptions.

Parameters and argument kinds

Python supports positional and keyword arguments. Keyword-only parameters, introduced after *, make important options explicit. Positional-only parameters can be declared before /, which is useful when parameter names are not part of a public API.

def connect(host, /, port=443, *, timeout=5.0, secure=True):
    """Example interface with positional-only and keyword-only arguments."""
    return (host, port, timeout, secure)

settings = connect("example.org", timeout=2.0)

Do not use a mutable object as a default argument when you expect a new object for each call. Default values are evaluated once when the function is defined:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Scope and local names

Names assigned inside a function are local by default. Function calls create local namespaces, allowing the same parameter name to be used in different functions. Return values are a clearer way to communicate results than changing global variables. Chapter 13 develops scope, closures, and name resolution in more depth.

Functions as values

def apply_twice(function, value):
    return function(function(value))

result = apply_twice(lambda number: number + 3, 4)
print(result)  # 10

Functions are first-class objects: they can be stored, passed as arguments, and returned. A lambda creates a small anonymous function expression; use def when logic needs a name, documentation, or multiple statements.

Beginner-to-master progression

Exercises

  1. Write a function that converts seconds into hours, minutes, and seconds.
  2. Create a function with a required positional parameter and a keyword-only formatting option.
  3. Write a function that accepts another function and applies it to every item in an iterable.
  4. Document the preconditions, result, and possible errors for one function you wrote.

Argument binding and API evolution

Python binds positional arguments from left to right and keyword arguments by parameter name. A function can collect extra positional arguments with *args and extra keyword arguments with **kwargs, but a public function that accepts anything without validation can make mistakes harder to detect. Use these forms for forwarding or genuinely extensible interfaces, not as a substitute for a clear signature.

def build_message(subject, *details, urgent=False, **metadata):
    lines = [subject, *details]
    if urgent:
        lines.insert(0, "URGENT")
    return {"text": "\n".join(lines), **metadata}

Adding a keyword-only option is often a safer API evolution than adding another positional parameter, because existing positional calls retain their meaning. Keep parameter names stable when callers are expected to use keywords. A function signature is a contract, so document which arguments may be omitted and what each default means.

Annotations and generic functions

Annotations can describe inputs and outputs, but runtime enforcement is not automatic. Static tools use them to reason about calls, branches, and values. The typing module provides constructs such as TypeVar, Protocol, Callable, and Iterable. Prefer the narrowest interface needed: an iterable is more flexible than a list if the function only loops once.

from collections.abc import Iterable

def total_lengths(words: Iterable[str]) -> int:
    return sum(len(word) for word in words)

Because this implementation consumes its input once, it can accept a generator. If the function loops over the input twice, an arbitrary iterable may be exhausted after the first pass. Type the contract that the implementation actually requires.

Pure functions, caching, and testing

A pure function depends only on its arguments and returns a result without changing external state. Pure functions are easy to test and cache. functools.lru_cache can memoize calls whose arguments are hashable, but a cache retains results and can consume memory. Choose a bounded size or clear the cache when its lifetime should end.

Side-effecting functions are still necessary for I/O and state changes. Keep them at the edges of a program and call pure functions for calculations where possible. This separation makes tests faster and allows the core behavior to be reused in different interfaces.

Mastery checkpoint

  1. Annotate a function using Iterable or Sequence according to its actual behavior.
  2. Design a public function so optional settings are keyword-only and defaults are stable.
  3. Separate a calculation from file or terminal I/O and test the pure function independently.
  4. Explain the memory-lifetime trade-off of an unbounded memoization cache.
0% read