Pure functions: same input, same output, no surprises
A pure function's result depends only on its arguments, and calling it changes nothing outside it. Such functions are easy to test, reuse and reason about. Side effects (changing inputs, globals, files, the clock) are kept at the edges of the program.
- Name the side effects and hidden inputs that make a function impure
- Return new data instead of changing the arguments, and check that inputs are unchanged
- Turn hidden inputs, such as today's date or a global setting, into parameters
A function is pure when two things hold. Its result depends only on its arguments: no global settings, no datetime.now(), no random numbers, no files. And calling it has no side effects: it doesn't change its arguments, globals or files, and doesn't print. Call it twice with the same arguments and you get the same answer, and nothing else in the program can tell it ran.
That makes it easy to test (no setup, no clean-up), safe to reuse anywhere, and safe to cache (see Decorators & Context Managers). Programs still need side effects, such as reading files and writing reports, but they can keep them in a thin outer layer and do the real work in pure functions.
Run each example to see what it prints. Running examples doesn't count toward progress.
def add_vat_in_place(basket, rate): for line in basket: line["price"] = line["price"] * (1 + rate) return basket def with_vat(basket, rate): return [{**line, "price": line["price"] * (1 + rate)} for line in basket] basket = [{"sku": "GRO-41017", "price": 8.0}, {"sku": "TOY-54564", "price": 25.0}] print(with_vat(basket, 0.2)) print(basket) add_vat_in_place(basket, 0.2) add_vat_in_place(basket, 0.2) print(basket)
Hidden inputs are just as troublesome. A function that calls date.today() gives a different answer tomorrow, so a test written today fails next week. Make the date a parameter: the caller passes date.today() in production and a fixed date in tests.
from datetime import date def days_left(best_before, today): return (date.fromisoformat(best_before) - today).days def expiring(products, today, within_days): return [p["code"] for p in products if 0 <= days_left(p["best_before"], today) <= within_days] stock = [ {"code": "5010029000023", "best_before": "2026-11-02"}, {"code": "20621483", "best_before": "2026-10-25"}, {"code": "0016000170032", "best_before": "2026-10-18"}, ] print(expiring(stock, date(2026, 10, 20), 7)) print(expiring(stock, date(2026, 10, 30), 7))
When to use it: for all calculation, conversion, filtering and formatting, which is most of a program. Keep reading files, calling APIs, printing and writing databases in a few outer functions. Mistakes to avoid: changing a list or dictionary you were given; mutable default arguments (see Functions), which keep state between calls; reading globals or the clock inside a calculation; and printing from a function whose job is to compute.
- A pure function's result depends only on its arguments, and calling it changes nothing outside it.
- Return new lists and dictionaries instead of changing the ones you were given.
- Pass hidden inputs (today's date, settings) in as parameters; keep printing and I/O at the edges.
- Next:
functools.partial, which makes a new function from an existing one by fixing some arguments.