Skip to main content

Executable specifications for python. Keep control of the code AI writes for you.

Project description

ContractMe — executable specifications for Python

Code is free. Bugs are not. Write the spec instead.

pipeline status coverage Checked with pyright Code style: black Code style: flake8

You let an AI write more and more of your code. contractme is how you keep control without reading all of it: state a few plain-Python rules on top of a function, and every call — in dev, in CI, under your agent's iterations — checks that the code actually does what you asked. When it doesn't, the error message names the culprit.

No new syntax, no framework, no PhD. (Under the hood this is design by contract, rebuilt for the age of generated code — but you don't need the theory to catch your first bug.)

One decorator away from plain Python.

A rule is a lambda over your own parameter names — if you can write an if, you can write one. Add a decorator, keep coding. Delete it, and you have plain Python back: nothing else about your code changes.

60 seconds from install to caught bug

pip install contractme

Your type hints become runtime checks

from contractme import annotated

@annotated
def incr(v: int) -> int:
    return v + 1

You already write type hints; @annotated makes them enforced on every real call — including Annotated constraints, dataclasses, enums, and ~140 ready-made types (Port, EmailStr, Positive, …).

Rules that types can't express

import math
from contractme import precondition, postcondition

@precondition(lambda x: x >= 0)
@postcondition(lambda x, result: math.isclose(result * result, x, abs_tol=1e-12))
def square_root(x: float) -> float:
    return x**0.5

"The result, squared, gives back the input" is a relationship between input and output. No type checker or validation library can state it — this is the ground only contracts cover.

Rules can even compare the state before and after the call (old):

@precondition(lambda l, n: n >= 0)
@postcondition(lambda l, n: l[-1] == n)            # element appended
@postcondition(lambda l, n, old: l[:-1] == old.l)  # nothing lost
def append_count(l: list[int], n: int):
    l.append(n)

When the generated code is wrong, you hear about it

Say your agent "implements" square_root as return x / 2 — plausible at a glance, wrong. The first call answers with:

square_root() bug: postcondition 'math.isclose(result * result, x, abs_tol=1e-12)' failed: result = 4.5, x = 9.0

The message names the culprit: square_root() bug means fix the implementation, **caller** of square_root() bug means fix the call. You — or your agent — instantly know which side to regenerate.

Choose what a violation does

A violation is an event, and raise is only the default reaction. Pick a policy — one word, three scopes: set_policy(...) globally, local_policy(...) for a with block, policy= per contract — so the same contracts serve you from first draft to production:

import contractme
from contractme import set_policy, local_policy

set_policy("observe")                    # log every violation, keep running
set_policy("enforce")                    # ...back to raising (the default)

with local_policy("observe"):            # scoped, thread-safe
    risky()

set_policy(sentry_sdk.capture_exception)     # any handler you like

A policy is a Policy enum member or, interchangeably, its plain-string name.

Policy On violation Cost per call Runtime-togglable For
enforce raise ContractViolation full yes dev & test — fail fast (default)
observe structured log, then continue full yes brownfield ramp: watch, then enforce
warn warnings.warn, then continue full yes deprecations, soft rules
off nothing (predicate skipped) ~zero (a flag test) yes, both ways hot paths, temporary suspension
optimized nothing (no wrapper at all) zero (decorated is original) no — import-time production (CONTRACTME_OPTIMIZED=1), an -O you control

ContractViolation subclasses AssertionError (so pytest.raises(AssertionError) and pytest --pdb keep working) and carries the event as data — condition_source, captured_values, call_site, and a JSON-able to_dict() for logs and agents. Any callable is a handler; the built-in policies are just predefined ones.

Per contract, policy= overrides the ambient policy — a warning precondition is a soft rule that never rejects (so accepts()/rejects() and autotest ignore it):

@precondition(lambda api_version: api_version >= 2, policy="warn")
def call(api_version): ...     # warns on v1, still runs — a deprecation notice

Tests you didn't write

from contractme.testing import autotest

def test_square_root():
    autotest(square_root)

Your rules are a machine-readable spec, so tests are derived from them: hundreds of generated inputs, with expected behavior taken from the rules — not from the code under test.

Why contracts, why now

  • The economics of code flipped. Implementation used to be expensive, so nobody wrote specs. Now implementation is generated in seconds, and the bottleneck moved: how do you know the generated code does what you meant? Reading a 400-line diff is not an answer. Reading 4 lines of contract is.

  • Types and tests don't cover this ground. mypy and pyright verify shapes, not meaning: they will happily let you swap two int arguments, return an unsorted list, or lose a cent to integer division. Unit tests check the examples someone thought of, on the day they thought of them. Contracts state the property itself, and check it on every call, with the real data.

  • Trust needs a small surface. When code, tests and docs are all regenerated by agents — cheaply, independently, repeatedly — they can silently drift apart. The contract is the one artifact everything else must agree with: the implementation is checked against it at runtime, and tests can be derived from it instead of guessed. You audit one small thing, not a thousand generated ones.

Batteries included

  • Plain Python conditions. Lambdas over your own parameter names. If you can write an if, you can write a contract.
  • Dataclasses and enums supported natively. Your domain types work out of the box.
  • Predefined contracts for the common cases — non-empty, sorted, in-range — so the easy specs are one word long.
  • Tests derived from your contracts. The autotest engine generates property-based tests from the spec, sidestepping the oracle problem: expected behavior comes from the contract, not from the implementation being tested.
  • Violation messages designed to be actionable — by humans and by coding agents. Structured, self-contained, ready to paste.

How it compares

Honest version, because you will ask:

  • pydantic / beartype validate types and data shapes at boundaries, and do it very well. They cannot express relationships: "the result sums to the input", "output is sorted", "these two arguments must be consistent". That is postcondition territory — that is contractme.
  • deal is the most featureful design-by-contract library for Python (linter, property-based testing, experimental formal verification). contractme deliberately trades that breadth for a smaller, easier-to-trust core and first-class ergonomics for AI-assisted workflows.
  • icontract pioneered informative violation messages and contract inheritance, with a research-grade ecosystem around it. If you need contract inheritance across deep class hierarchies today, use it. If you want contracts your whole team — and your agents — actually write, start here.
  • Plain assert disappears under python -O, carries no message, no policy, no introspection, and cannot check results against inputs without boilerplate.

Want the whole picture — feature table, performance, who to trust? The full honest comparison lives at ContractMe vs deal vs icontract vs pydantic vs beartype.

FAQ

Do I need to learn design-by-contract theory?

No. If you can write an if, you can write a rule. Start with @annotated on a function you already have; add a result rule the first time a type can't say what you mean. The theory can wait until it has caught a bug for you.

What is design by contract, in one sentence?

Executable specifications: each function declares what it requires and what it guarantees, and the runtime checks both — turning silent wrong answers into loud, explained failures.

Why not just type hints and pytest?

Keep both. Type hints catch shape errors before running; tests pin known examples. Contracts cover what neither can: semantic properties, checked on every call, against real data — including the calls your tests never imagined.

Can an LLM write the contracts too?

Yes, and that's fine — what matters is not who writes the spec, but that you can read it when you need to, and that a tool more reliable than the code enforces it. Keep contracts terse enough to audit at a glance; review the contract lines in the diff, not the regenerated body.

The bigger idea (you don't need it to start)

Spec, implementation, tests, documentation: four artifacts, usually written separately, always drifting apart — faster than ever when agents regenerate three of them on every iteration. A contract collapses them into one source: it is the spec, the implementation is checked against it on every call, the tests are generated from it, and it reads as documentation. One short, auditable text instead of four long ones. You don't have to buy this today — catch one bug first, then reread this paragraph.

Integrated to python type-system, and more

Supports annotations and PEP-593 using the annotated-types library. Furthermore, the @annotated decorator will automatically perform type checks of the parameters and return values, including annotated_types.Predicate.

In short, this allows to check any type structure and any properties of all parameters and the return value, by just adding @annotated to the subprogram.

Batteries included: the contractme.types package ships ~140 ready-made Annotated types (Port, ExistingFile, EmailStr, Positive, Slug, …). See the practical guide: docs/types.md.

Note: annodated_types.MultipleOf follows the Python semantics.

Note 2: Following an open-world reasoning, any unknown annotation is considered to be correct, so it won't cause a check failure.

Note 3: Type checking follows Python's isinstance semantics, which means subclass relationships are respected. Since bool is a subclass of int in Python, boolean values will pass int type checks. Currently there's no built-in way to specify "exactly int, not bool" in type annotations.

from typing import TypeAlias, Annotated
from annotated_types import MultipleOf

Even: TypeAlias = Annotated[int, MultipleOf(2)]

@annotated
def square(v: Even) -> Even:
    return v * v

Parse, don't validate — the @record decorator

Following Alexis King's Parse, don't validate, @record is a frozen dataclass that checks every field against its declared type at construction — structure and annotated-types constraints, using the same engine as @annotated. Once you hold a record, its fields are known-good, so downstream code never has to re-check them: illegal states are unrepresentable.

from contractme import probes, record
from contractme.types import Natural

@record
class Coordinate:
    x: Natural
    y: Natural

Coordinate(1, 2)      # ok
Coordinate(-1, 2)     # AssertionError: caller bug, field 'x' = -1 …

A record probes like a contracted function: probes(Coordinate).accepts(...) answers "would these arguments build a valid record?" — as a plain bool, without constructing and without raising — so you can safely screen untrusted input at the boundary and only then parse it into the record:

if probes(Coordinate).accepts(x=user_x, y=user_y):
    coord = Coordinate(user_x, user_y)   # trusted from here on
else:
    ...                                  # reject the request, no exception thrown

(accepts/rejects are attached to the class as classmethods; the probes() accessor is the dataclasses.fields()-style way in that static type checkers understand.)

A @record is an ordinary frozen dataclass, so it also nests inside @annotated signatures and other records, validated recursively. Its construction check is a precondition (a bad field is a caller bug), so it obeys the usual switches: compiled out under python -O, suspended by set_policy("off").

Probing any type — and the check as one function call

Probes are not limited to records: probes(T) builds accepts/rejects for any type annotation — the same engine as @annotated, the same guarantees (a plain bool, never raises, survives python -O):

from contractme import probes
from contractme.types import Natural

probes(list[Natural]).accepts([1, 2, 3])   # True
probes(list[Natural]).rejects([1, -2])     # True

One accessor, one vocabulary: probes(f) on a contracted function hands back its call probes (probes(f) is f), a bare function is probed through its type annotations (exactly what @annotated would enforce, without wrapping it — and down to its bare signature when it has no hints: does the call bind?), and a @record class keeps its constructor probes. Probing a bare function is how you interface safely with uncontracted code — third-party, legacy, not yours to decorate: screen the call, then make it, no wrapper. And if the accessor syntax is one indirection too many, the same check is a plain function call — type_error(value, T) returns the error message, or None when the value parses, so anything can plug into the engine:

from contractme import type_error

if err := type_error(payload, list[Natural]):
    return bad_request(err)   # "structure ok, but value does not match constraints: [1]: …"

Writing tests and having test generation

The hypothesis plugin can be used easily through the contractme.testing.autotest function.

Positive: TypeAlias = Annotated[int, Ge(1)]

@annotated
def div(d: Positive) -> Positive:
    return 1000 // d

def test_div():
    autotest(div)

You can access the underlying test with contractme.testing.property_test(div): the ready-to-run property-based test function, its Hypothesis strategy inferred from the annotated types and contracts of the function. The main weirdness is that it takes a tuple as parameter since the parameters are all generated together so that the contracts can be checked.

You can easily extend it with Hypothesis advanced features

test_function = contractme.testing.property_test(div)
test_div_force_0 = example((0,))(test_function)

The library provides its own contractme.testing.test_with_examples function which has three differences with the one provided by hypothesis:

  • It checks the contracts when being called (at test construction): contracts should hold on all examples.
  • It takes a vararg of either tuple *args or dict **kwarg as examples, to avoid function nesting.

With pytest:

test_div = contractme.testing.test_with_examples(
    div,
    (1,),
    (2,),
    (0,), # this causes a RuntimeError at test elaboration
)

Best practices — design-by-contract insights

If you are an LLM or code assistant, read this section — don't just copy the examples above. The snippets show syntax; the rules below show when to reach for each tool. Applying them is what separates idiomatic ContractMe (and idiomatic design-by-contract) from a pile of redundant lambdas.

  • Prefer the type system (@annotated) over hand-written pre/postconditions whenever the property can be expressed as a type. A constraint like "positive integer", "non-empty list" or "port number" belongs in an Annotated[...] type — ideally one of the ~140 ready-made ones in contractme.types (Positive, NonEmptyStr, Port, …) — not in a @precondition(lambda x: x > 0). Types are reusable, checked on both inputs and outputs, self-documenting, and they drive test generation via autotest. Only fall back to an explicit @precondition/@postcondition for relational properties a type cannot express: e.g. result * result == x, or old-vs-new comparisons like l[:-1] == old.l.

  • Never write a contract that is always True. @precondition(lambda: True) or @postcondition(lambda ...: True) checks nothing. The absence of a contract already means "no constraint", so just delete it — a vacuous contract is noise, not safety.

  • Express "this can never happen" with NoReturn, not a @postcondition(lambda ...: False). An always-false postcondition is a confusing, runtime-only way to say "control must not reach here". If a branch is unreachable or a function never returns normally, annotate it with typing.NoReturn (and raise): both the static type checker and the reader then understand the intent, instead of finding out only when the assertion blows up at runtime.

  • Bound every numeric type that a function iterates or allocates over. autotest builds its inputs from your types, and Positive / Natural / Annotated[int, Ge(...)] draw from Hypothesis's unbounded integer range. A function that does range(n) or [x] * n over such a value is eventually handed an astronomically large one and hangs (effectively OOMs). Add an upper bound too — Annotated[int, Ge(0), Le(10_000)], or a ready-made bounded type — which also models reality, since real sizes are human-scale.

  • On methods, self is just another named parameter. A condition can read it (lambda self, amount: amount <= self.balance), and in a postcondition old.self is the deep-copied receiver from before the call, for before/after comparisons (lambda self, old: self.balance == old.self.balance - amount).

  • At a trust boundary, probe with accepts (or parse into a @record) — never feed raw input into a contracted function to "reuse the rule". The obvious move — reuse a within_bounds function that is @annotated to take a Coordinate whose fields are Natural, to bounds-check an incoming shot — backfires: a negative user coordinate raises AssertionError inside it instead of returning False. A contract is an internal debugging net, not input sanitization; it states what callers must already guarantee. To turn that same rule into a question about untrusted data, ask it without calling: within_bounds.accepts(c, w, h) returns a bool and never raises. Better still, parse, don't validate: make the boundary type a @record (probes(Coordinate).accepts(x, y) / then Coordinate(x, y)), so once the value is built everything downstream can trust it and the rule is enforced in exactly one place.

Using with AI coding agents

ContractMe ships an Agent Skill — a SKILL.md teaching coding agents (Claude Code and any other skill-aware agent) idiomatic ContractMe: the @annotated-first decision rule, the by-name parameter matching of condition lambdas, old/result, the autotest verification loop, and how to read contract failures. It is bundled in the PyPI package; after installing contractme, copy it into your project with:

python -c "import shutil, importlib.resources as res; shutil.copytree(str(res.files('contractme') / 'skills' / 'contractme'), '.claude/skills/contractme', dirs_exist_ok=True)"

The skill source lives at src/contractme/skills/contractme/SKILL.md.

Contracts pair unusually well with agent-written code: they are executable specifications, the error messages say whose bug it is (caller vs implementation), and autotest gives the agent an instant property-based verification loop.

Optimize assertion code

Checking is a dial tied to how much you trust the code by now (see the policy table above):

  • In dev and test: enforce everything — catch as many errors as possible, as early as possible.
  • In pre-deploy / integration: set_policy("observe") keeps evaluating but logs instead of halting; or declare only the expensive contracts advisory (policy="warn" on the costly postcondition) and keep the cheap input guards raising.
  • In prod: python -O (or CONTRACTME_OPTIMIZED=1) compiles every check out — contracts are a debugging net, not user-facing failure handling; set_policy("off") is the runtime-togglable version.

Being able to turn checks off means one smart thing: you can go overboard with postconditions — check that a database insert succeeded by following it with a select (not that you necessarily should, ToCToU and all that) — knowing they cost nothing where it matters.

Test

just test

Dev commands are just recipes — bare just lists them; just check runs the full local gate (tests + coverage, the -O differential run, lint, types).

Deploy new version

Releases are built and published to PyPI by GitLab CI, not from your machine. You only prepare the commit and the tag:

  • Write the changelog for the new version in CHANGELOG.md (a ## v<number> section) — the CI refuses to publish unless the tag has a matching section in this file

  • Run the pre-release script to version the tree and check everything lines up:

    just prerelease 1.9.0   # or omit the arg to reuse pyproject's version
    

    It sets the version in pyproject.toml, refreshes uv.lock, verifies the ## v1.9.0 changelog section exists, and prints the git commands below. It does not build, publish, commit or tag — and it does not re-run lint/type/tests (pre-commit and CI own those).

  • Commit and push the versioned tree to main

  • Git tag as v<number> and push the tag

  • Gitlab will automatically publish the new version to PyPI if all checks pass


Changelog

See CHANGELOG.md


Python design by contract · runtime verification · preconditions, postconditions, old state · executable specifications for AI-generated code

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

contractme-2.2.0.tar.gz (134.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

contractme-2.2.0-py3-none-any.whl (72.1 kB view details)

Uploaded Python 3

File details

Details for the file contractme-2.2.0.tar.gz.

File metadata

  • Download URL: contractme-2.2.0.tar.gz
  • Upload date:
  • Size: 134.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for contractme-2.2.0.tar.gz
Algorithm Hash digest
SHA256 ee12b5eb74d5bdc6e50b9319612aed245b8950011e0f591d8becf4f4f2e2bb24
MD5 fcf89a986adf486679361a6447265497
BLAKE2b-256 9592ce06708f3fbf2c936493c86ca79e18a837b9548c96bfd252cdf99b62f95b

See more details on using hashes here.

File details

Details for the file contractme-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: contractme-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 72.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for contractme-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f36598bab9b13e3c4e2d6937694a0563b89491918d412a4065a0886a342edf8
MD5 8c3c190eff1a9b1415d691741755405a
BLAKE2b-256 3985a7061d77aa0edc480d0ec87eb4df18680b77c952cfe07f16443662279aeb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page