← Practical Python
03 / 10 · 23 MIN

Small functions, clear contracts

Define inputs, results, and side effects before writing the implementation.

A contract you can explain

A function should have a recognizable responsibility. Distinguish returning a result from printing it: return hands a value to the caller, while print writes a representation. A function without an explicit return returns None. Type annotations document intent but do not automatically validate input.

Mutable defaults

Default arguments are evaluated when the function is defined. A list used as a default can accumulate mutations across calls. Use None and create the list inside the function when each call should receive a fresh collection.

Guided workplace application

In a collect function, items=None allows creation of a collection when the caller omits it. Use if items is None so an empty list the caller intends to fill is not replaced. The items = items or [] pattern violates that contract because it treats an empty list as absence. If the function should be pure and return a new collection, document that different contract and adapt callers. Keep validation separate from effects such as sending email or writing files so the rule can be reused. Type annotations help communicate inputs and results, but in an ordinary function they do not automatically convert or reject external data. A validation layer must implement that behavior explicitly.

Preserve a failure cause

A function can convert a technical exception into a domain exception without losing causality. Use raise BatchImportError("invalid input") from exc when the original cause should remain associated. Do not return an error string in place of a normal result if the caller expects an exception. If the message contains sensitive data, create an appropriate summary; preserving chaining and deciding what users see are separate responsibilities.

Value capture and explicit parameters

A function created in a loop may read its outer variable only when called, finding the loop’s final value. For per-ID callbacks, capture the value during creation or pass it explicitly; changing lambda to def alone does not fix it. In a signature, parameters after * are keyword-only. Requiring dry_run by name reduces call ambiguity but does not implement absence of effects: the function body and tests must meet that contract.

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

A validation function should return the errors it finds without sending email or changing files. Separating those effects lets the same rule be reused and tested in an API and a batch process.

Common pitfalls

Using mutable defaults; replacing supplied empty lists; treating annotations as validation.

Related topics: Errors and files with context · Precision, external data, and time

Take this idea with you

Make inputs and effects explicit; use None when an optional argument needs a fresh mutable object.

Create account

Reference: Python 3.14: Control flow and functions · Python 3.14; DR Python 2026.3