Skip to content

contract

A contract is the set of promises an interface makes to the code that calls it, covering what inputs it accepts, what it returns or raises, and what stays true once it finishes. It applies at every scale, from a single function to the API between two services, and it holds whether or not anyone wrote it down.

A contract is wider than a function signature. The signature names the parameters and their type hints, and the rest of the agreement lives in the docstring, the exceptions, and the observable behavior:

Language: Python
def withdraw(account: Account, amount: int) -> int:
    """Debit amount from account and return the new balance.

    Raises InsufficientFunds when amount exceeds the balance.
    """

Callers of withdraw() depend on four separate promises here. The signature carries only the first, the argument order and the types. The docstring adds two more, that the return value is the new balance rather than the amount debited, and that overdrawing raises InsufficientFunds rather than a bare ValueError. The fourth, that the balance never goes negative, is written down nowhere and gets relied on anyway.

How It Shows Up in Practice

A Python developer rarely meets a contract as a formal document. It surfaces as the reason a change is harder than it looks, in review comments, release notes, and incident threads:

  • Code review: “This changes the contract” is shorthand for a change that still imports cleanly but will break callers, such as reordering parameters or narrowing a return type.
  • Release numbering: Semantic versioning exists to signal contract changes. A major bump is a maintainer stating that a promise was withdrawn.
  • Service boundaries: Between microservices, the contract is usually written down as an OpenAPI or Protocol Buffers schema and generated into client code on both sides.
  • Contract testing: Tools such as Pact let a consumer record what it expects and let the provider verify it independently, catching drift without a full integration test environment.
  • Deprecation: A DeprecationWarning plus a documented timeline is how a maintainer retires a promise without breaking callers overnight.

What a contract deliberately excludes matters as much as what it includes. Anything outside the public API surface stays free to change, which is why maintainers mark internals with a leading underscore. A contract that promises too much leaves nothing safe to refactor.

Preconditions, Postconditions, and Invariants

Bertrand Meyer built this idea into the Eiffel language and began writing about it in 1986 as design by contract. It splits an agreement into three parts: preconditions the caller owes before the call, postconditions the callee guarantees on return, and invariants that hold both on entry and on exit.

The split is useful mainly for assigning blame. A violated precondition is a bug in the caller, and a violated postcondition is a bug in the callee. Python enforces none of it at runtime, so teams approximate it with assertions, explicit validation at the boundary, and static type checkers.

Step through a single withdraw() call below, breaking one promise at a time, to see where each check happens and who ends up at fault:

Interactive diagram — enable JavaScript to view.

In Python the contract often carries the whole relationship between two objects, because duck typing accepts any object that behaves correctly, whatever its declared type. To read about writing that expectation down explicitly, see protocol.

Implementing an Interface in Python

Tutorial

Implementing Interfaces in Python: ABCs and Protocols

Learn how to implement interfaces in Python using abstract base classes, Protocols, and duck typing, and enforce method contracts cleanly.

advanced python

For additional information on related topics, take a look at the following resources:

Have a question about this? Mentor AI can show you examples, compare related terms, and point you to tutorials.


By Martin Breuss • Updated Oct. 1, 2026