PYTHON • SOFTWARE CRAFT AND COMMUNICATION

Function Contracts & Invariants — State input/output contracts and invariants for functions

Write functions that clearly promise what they expect, what they deliver, and what always stays true.

Historical Context & Motivation

Have you ever used a function written by someone else and had no idea what values you were supposed to pass in, or what you'd get back? Maybe you passed a string where the function expected a number, and the whole program crashed. This frustrating experience is exactly the problem that function contracts were designed to solve. The idea of formally specifying what a function requires and what it guarantees has roots stretching back decades in computer science.

1969
Hoare Logic
Tony Hoare published a landmark paper introducing Hoare triples, which describe a program's behavior using a precondition, a command, and a postcondition. This laid the mathematical groundwork for reasoning about code correctness.
1986
Design by Contract
Bertrand Meyer coined the term Design by Contract while creating the Eiffel programming language. He treated the relationship between a function and its caller like a legal agreement—each side has obligations and benefits.
2000s
Assertions in Python
Python adopted the assert statement and docstrings as lightweight tools for expressing contracts. Libraries like icontract later added decorator-based contract enforcement.
2014+
Type Hints & Modern Contracts
Python 3.5 introduced type hints (PEP 484), giving developers a way to annotate expected input and output types. Combined with docstrings and assertions, modern Python supports expressive contracts without leaving the language.

The core question these milestones address is simple yet powerful: How can we make functions self-documenting so that anyone—including our future selves—knows exactly how to use them correctly? Function contracts and invariants answer that question by turning informal expectations into explicit, checkable agreements.

Core Principles & Definitions

A function contract is an agreement between the function and its caller. The caller promises to provide valid inputs, and the function promises to return a correct result. If either side breaks the agreement, we know exactly where the bug lives. This idea breaks down into three key parts: preconditions, postconditions, and invariants.

1

Precondition

A precondition states what must be true before the function runs. It's the caller's responsibility. For example, a square-root function might require that the input is non-negative.
2

Postcondition

A postcondition states what must be true after the function finishes. It's the function's responsibility. The square-root function guarantees that its result squared equals the original input (within rounding).
3

Invariant

An invariant is a condition that stays true throughout execution—before, during, and after the function runs. For instance, a bank account's balance should never go negative, no matter which function modifies it.
4

Type Contract

A type contract specifies the data types a function expects and returns. Python type hints like def add(a: int, b: int) -> int serve as a lightweight type contract.
✦ KEY TAKEAWAY
Think of a function contract like ordering food at a restaurant. You (the caller) must order from the menu (precondition). The kitchen (the function) promises to deliver what you ordered, cooked properly (postcondition). The restaurant's health code (invariant) must hold true the entire time—before, during, and after your meal is prepared. If you order something not on the menu, that's your mistake. If they serve you raw chicken, that's theirs.

Visual Explanation

The diagram shows the three-part contract: the caller satisfies preconditions (left), the function maintains invariants during execution (center), and the result satisfies postconditions (right). Below, the Python code shows how type hints, assertions, and comments express each part.

Notice how the diagram separates responsibilities. The caller box on the left holds the preconditions—these are promises the caller must keep. The function box in the center is where work happens, and the dashed pink border represents the invariant that must remain true throughout execution. The result box on the right lists postconditions that the function guarantees upon completion. This visual pattern applies to every function you'll ever write.

How Contracts Work in Python

Three Tools for Expressing Contracts

Python gives you three main tools to express function contracts: type hints, docstrings, and assert statements. Each serves a different purpose, but they work best together. Type hints state the data types expected and returned. Docstrings describe the contract in human-readable language. Assert statements enforce the contract at runtime, crashing the program with a helpful message if something goes wrong.

Type Hints as Contracts

Type hints appear in the function signature and communicate what types the function expects and returns. While Python does not enforce these at runtime by default, tools like mypy can check them before your code runs. The syntax follows a straightforward pattern.

TYPE HINT SYNTAX
def function_name(param₁: Type₁, param₂: Type₂) → ReturnType:
Each parameter is annotated with its expected type using a colon. The return type follows the -> arrow. Common types include int, float, str, bool, and list[int].

Assert Statements for Runtime Checking

An assert statement evaluates a condition, and if that condition is False, it raises an AssertionError with an optional message. You place precondition asserts at the top of the function and postcondition asserts just before the return statement. This gives you a safety net that catches contract violations while your code runs.

ASSERT SYNTAX
assert condition, "Error message describing the violated contract"
If condition evaluates to True, execution continues normally. If False, the program halts and prints the error message, immediately telling you which contract was broken.

Docstrings for Human-Readable Contracts

A docstring is a triple-quoted string placed as the first line inside a function. It describes what the function does, what it expects (preconditions), what it returns (postconditions), and any invariants. The widely-used Google style includes sections for Args, Returns, and Raises. Docstrings don't enforce anything automatically, but they communicate your contract to every developer who reads your code—and you can view them with help(function_name).

Types of Contracts & Invariants

Not all contracts are created equal. Some are simple type checks, while others express complex relationships between inputs and outputs. Understanding the different categories helps you choose the right level of specificity for each function you write.

The three columns classify contract types: preconditions (what the caller must provide), postconditions (what the function guarantees), and invariants (what remains true throughout). Each row shows a specific sub-type with a Python expression example.

A loop invariant is especially useful when writing loops inside a function. It describes a condition that is true at the start of every iteration. For example, in a function that sums a list, the invariant might be "the variable total holds the sum of all elements processed so far." If you can prove the invariant holds before the loop starts, is maintained by each iteration, and implies correctness when the loop ends, then you've essentially proven your function works.

💡 PRO TIP
Start by writing your contracts in a docstring before you write any code. This forces you to think clearly about what your function should do. Then add assert statements to enforce the most critical conditions. Finally, add type hints to the signature. This "contract-first" approach often prevents bugs before they happen.

Worked Example

Let's build a function from scratch, specifying its full contract. We'll write a function called calculate_average that computes the arithmetic mean of a list of numbers.

Building a Fully Contracted Function: calculate_average
1
Step 1 — Identify the ContractBefore writing any logic, we determine the contract. Preconditions: The input must be a list of numbers (int or float), and the list must not be empty (to avoid division by zero). Postconditions: The returned value must be a float, and it must be between the minimum and maximum values in the list (inclusive). Invariant: The running total at any point during summation equals the sum of all elements processed so far.
Pre: non-empty list of numbers | Post: min ≤ result ≤ max | Inv: running_sum = Σ processed
2
Step 2 — Write the Signature with Type HintsWe express the type contract in the function signature: def calculate_average(numbers: list[float]) -> float:. This tells anyone reading the code that the function expects a list of floats and returns a float. While Python won't enforce this at runtime, tools like mypy will catch type errors before you run the code.
def calculate_average(numbers: list[float]) -> float:
3
Step 3 — Write the DocstringNext we add the docstring, spelling out every part of the contract in plain English: """Calculate the arithmetic mean of a list of numbers. Args: numbers: A non-empty list of numeric values. Returns: The arithmetic mean as a float. Raises: AssertionError: If the list is empty. Contract: Precondition: len(numbers) > 0 Postcondition: min(numbers) <= result <= max(numbers) Invariant: running total equals sum of processed elements """
Complete docstring documenting Args, Returns, Raises, and Contract
4
Step 4 — Add Precondition AssertionsAt the top of the function body (after the docstring), we enforce preconditions with assert statements: assert len(numbers) > 0, "Precondition failed: list must not be empty". If someone calls calculate_average([]), the program will immediately crash with a clear message instead of silently returning NaN or causing a ZeroDivisionError.
assert len(numbers) > 0, "Precondition failed: list must not be empty"
5
Step 5 — Implement with Invariant AwarenessNow we write the logic. We initialize total = 0.0 and loop through each number, adding it to total. The loop invariant is that total always equals the sum of elements from index 0 up to the current index. After the loop, we compute result = total / len(numbers).
total = 0.0 for num in numbers: total += num # Invariant: total == sum of elements processed result = total / len(numbers)
6
Step 6 — Add Postcondition Assertions and ReturnBefore returning, we verify our postconditions: assert min(numbers) <= result <= max(numbers), "Postcondition failed: result out of range". Finally, we return the result. The complete function now communicates its full contract through type hints, a docstring, and runtime assertions.
assert min(numbers) <= result <= max(numbers) return result

Strengths & Limitations of Contracts

Function contracts are powerful, but like any tool they have trade-offs. Understanding both sides helps you decide how thoroughly to specify contracts in your own projects.

Comparing the strengths and limitations of function contracts in Python
AspectStrengthsLimitations
Bug DetectionAssert statements catch violations immediately at the exact line where the contract is broken, making debugging much faster.Asserts only catch bugs if the code actually runs through the failing path. They don't guarantee full coverage.
DocumentationContracts serve as living documentation—they describe what the function does and stay in sync with the code because they run with it.Writing thorough contracts takes extra time upfront. For trivial helper functions, this can feel like over-engineering.
PerformanceContracts add negligible overhead in most applications. Python's -O flag can disable asserts in production.Complex postcondition checks (like verifying a list is sorted) can slow down performance-critical code.
Team CollaborationContracts make expectations explicit, so teammates can use your function without reading every line of its implementation.Type hints and asserts don't replace actual communication. Misunderstood contracts can create a false sense of safety.
EnforcementType checkers like mypy can catch type contract violations before the code ever runs, saving debugging time.Python is dynamically typed, so type hints are not enforced at runtime by default. You need external tools to check them.
✦ KEY TAKEAWAY
Function contracts are like the safety features in a car. Seat belts, airbags, and lane-departure warnings add a small amount of cost and weight, but they dramatically reduce the severity of accidents. You don't remove them just because most drives are uneventful. Similarly, contracts add a small amount of code, but they save enormous debugging time when things go wrong—and they will go wrong.

Connection to Advanced Concepts

The contract-based thinking you've learned here is a foundation for several advanced topics in software engineering. As you continue studying computer science, you'll encounter these concepts again in more powerful forms.

How function contracts connect to advanced computer science concepts
What You LearnedAdvanced VersionWhat Changes
Type hints (int, str)Static type systems (TypeScript, Rust, Haskell)The compiler refuses to run code that violates type contracts, catching errors at compile time instead of runtime.
Assert statementsFormal verification (Dafny, SPARK)Mathematical proofs guarantee that contracts hold for all possible inputs, not just the ones you tested.
Docstring contractsProperty-based testing (Hypothesis library)Automated tools generate thousands of random inputs and verify that postconditions hold for every one.
Loop invariantsAlgorithm correctness proofsIn college-level algorithms courses, you formally prove that loop invariants imply the algorithm produces correct results.
Data invariantsClass invariants & OOP designIn object-oriented programming, class invariants ensure that an object is always in a valid state, no matter which methods are called.

The key insight is that the mindset of thinking in contracts matters more than any specific tool. Whether you're writing Python with assert statements or Rust with its type system, the underlying discipline is the same: clearly define what you expect, what you guarantee, and what must always be true. That discipline will serve you in every programming language and every stage of your career.

Practice Problems

PROBLEM 1 — CONCEPTUAL
A function called divide(a, b) divides a by b. Identify one precondition and one postcondition for this function. Explain why each is necessary.
PROBLEM 2 — BASIC
Write a complete function signature with type hints and one assert statement for a function called get_letter_grade(score) that takes an integer score (0–100) and returns a string letter grade.
PROBLEM 3 — INTERMEDIATE
Consider this function that finds the maximum value in a list: def find_max(items: list[int]) -> int: best = items[0] for item in items: if item > best: best = item return best Write a complete contract for this function including: (a) all preconditions, (b) all postconditions, and (c) the loop invariant. Express each as both English text and a Python assertion.
PROBLEM 4 — APPLIED
You are writing a function for a student gradebook application: def add_grade(gradebook: dict, student: str, grade: float) -> dict. The gradebook maps student names to lists of grades. Write the full function (with type hints, docstring, precondition asserts, postcondition asserts, and at least one invariant stated in a comment) that adds a new grade to an existing student's record.
PROBLEM 5 — CRITICAL THINKING
A classmate argues: "Assert statements are useless because Python lets you disable them with the -O flag, so they might not even run in production. We should just use if statements and raise exceptions instead." Write a thoughtful response that (a) acknowledges the valid point, (b) explains when asserts are still valuable, and (c) describes a scenario where raising an exception is better than using an assert.

Lesson Summary

A function contract is an explicit agreement between a function and its caller, consisting of three parts: preconditions (what the caller must provide), postconditions (what the function guarantees to return), and invariants (conditions that remain true throughout execution). In Python, you express contracts using type hints in the function signature, docstrings for human-readable documentation, and assert statements for runtime enforcement.

Thinking in contracts helps you design better functions, communicate your intentions to teammates, and catch bugs early. The concept traces back to Hoare logic (1969) and Design by Contract (1986), and it connects forward to advanced topics like static type systems, formal verification, and class invariants in object-oriented programming. Whether you write a simple helper function or a complex algorithm, always ask yourself: what are my preconditions, what are my postconditions, and what must always be true?

Varsity Tutors • Python • Function Contracts & Invariants