A powerful, expressive and lightweight design-by-contract framework
Project description
ContractMe
A lightweight and adaptable framework for design-by-contract in python
Example code
Here are some examples:
result
@precondition(lambda x: x >= 0)
@postcondition(lambda x, result: eps_eq(result * result, x))
def square_root(x: float) -> float:
return x**0.5
old
@precondition(lambda l, n: n >= 0 and round(n) == n)
@postcondition(lambda l, n: len(l) > 0)
@postcondition(lambda l, n: l[-1] == n)
@postcondition(lambda l, n, old: l[:-1] == old.l)
def append_count(l: list[int], n: int):
l.append(n)
Using annotations
@annotated
def incr(v : int) -> int:
return v + 1
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
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) return Positive:
return 1000 // d
def test_div():
autotest(div)
You can access the underlying hypothesis generator with contractme.testing.get_generator(div).
It's a pure hypothesis strategy generator, 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
generator_function = contractme.testing.get_generator(div)
# kinda weird to have this double call, but that's decorators for you...
test_div_force_0 = example((0,))(generator_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
*argsor dict**kwargas 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 anAnnotated[...]type — ideally one of the ~140 ready-made ones incontractme.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 viaautotest. Only fall back to an explicit@precondition/@postconditionfor relational properties a type cannot express: e.g.result * result == x, orold-vs-new comparisons likel[:-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 withtyping.NoReturn(andraise): both the static type checker and the reader then understand the intent, instead of finding out only when the assertion blows up at runtime.
Optimize assertion code
- In prod you can disable assertions, then these runtime checks wont run: you can have your cake and eat it too
- You have even more granularity of checks thanks to
contractme.contracting.ignore_preconditionsandcontractme.contracting.ignore_postconditions
In theory, the rule is that checks are activated depending on the trust you put in your software
- In dev: You run with all assertions, to catch as many errors as possible, as early as possible
- In pre-deploy / integration testing: you only run with the pre-conditions assertions: postconditions are typically costly, and you trust that you return the right result given the proper input
- In prod: you run with no assertion - those are meant for debugging, not user facing failure modes which are / should be handled properly in sanitization code
In practice, you might want to keep then on all of the time, but being able to turn them off means one smart thing: you can get overboard in checking with postcondition, knowing these can be turned off in integration conditions e.g. you can check that a database insert succeeded by following it with a select - not that you necessarily should, ToCToU and all that.
Test
uv run pytest
Deploy new version
Releases are built and published to PyPI by GitLab CI (the publish-to-pypi job), not
from your machine. You only prepare the commit and the tag:
-
Write the changelog for the new version in
README.md(a## v<number>section) — the CI refuses to publish unless the tag string appears in this file -
Run the pre-release script to version the tree and check everything lines up:
uv run python release/prerelease.py 1.9.0 # or omit the arg to reuse pyproject's version
It sets the version in
pyproject.toml, refreshesuv.lock, verifies the## v1.9.0changelog 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 -
In the tag's pipeline, trigger the manual
publish-to-pypijob: it runsuv build, checks the tree is clean (git diff --exit-code), anduv publishes to PyPI
Changelog
v1.9.3
- Update release pipeline uv image
- Copy logic from epycs' CI
- Release: fix by using
uv lock --checkinstead of git - Fix release pipeline by extracting it to a proper script file
v1.9.0
@annotatednow understands three new composite type shapes:- dataclasses as named products — the checker recurses into each field by name,
validating nested types and per-field
Annotatedconstraints - enums as sums of singletons — membership is an
isinstancecheck (soFlagcombinations stay valid), andAnnotatedconstraints on the member value (e.g. anIntEnum) are enforced typing.Protocolas structural products — basic duck typing where each declared member must be present (hasattr) and typed members are checked, with no nominalisinstance(neither inheritance nor@runtime_checkablerequired)
- dataclasses as named products — the checker recurses into each field by name,
validating nested types and per-field
- New standalone
typeshapemini-library: type meta-manipulation (no value inspection) that maps any annotation onto a small algebra viaChildMod(atom/product, option, repeat, record, enum, structural).typecheckandannotationsare now its first consumers, sharing one homomorphism instead of duplicating type-walking logic - Fix silent no-op under
from __future__ import annotations(PEP 563):@annotatednow resolves hints viaget_type_hints(..., include_extras=True)instead of reading the raw (stringized)__annotations__, so contracts are enforced even when the decorated module stringizes its annotations. Previously this silently disabled every check - Fix a silent-failure bug: under
from __future__ import annotations(PEP 563) every annotation is a string, so@annotatedread the raw__annotations__, found noAnnotatedmetadata, and silently enforced nothing. Hints are now resolved viaget_type_hints(..., include_extras=True), so contracts keep working in PEP-563 modules - New README section Best practices — design-by-contract insights: prefer
@annotatedtypes over hand-written lambdas, never write a vacuous contract, and useNoReturninstead of an always-false postcondition
v1.8.0
- New
contractme.typespackage: a large, batteries-included library of ready-madeAnnotatedtypes (numeric, text, paths, network, temporal, identifiers, system, structured-data, containers) usable out of the box with@annotatedand pydantic - Names follow Ada/SPARK (
Positive,Natural) and pydantic where applicable, with aliases pointing at the same objects - Reusable named predicates in
contractme.types.predicates(+PredicateFn/PredicateFactorytyping aliases) - Factories
Bounded[lo, hi]andSuffixPath[".csv", ...], and aSecretStrwrapper - Optional extras for richer validators:
cron(croniter),iso(pycountry),yaml(pyyaml) — lazily imported, not required toimport contractme.types typecheck: nestedAnnotatedconstraints (e.g.Annotated[str, LowerCase]) are now flattened, soannotated-typespredicate aliases compose naturally- New practical guide: docs/types.md
v1.7.0
- Refactored lambda source extraction (
show_source) for improved robustness when stripping surrounding parentheses - Added docstrings to public API modules for better discoverability
- Added CLAUDE.md with architecture guidance for AI-assisted development
typecheck: identifies types with no true origin in recursive type resolution- Fix unreachable test code detected by coverage
v1.6.0
- Fix huge bug where instance methods were not working anymore
- Add support for class methods
- Richer types stubs for pre / post (still very imperfect)
v1.5.1
- Minor contracted functions fix
v1.5.0
- Contracted functions have a correct return type.
v1.4.0
@annotated supports more complex types
- TypeAlias
- Recursive types
@annotated supports common nested data types
- tuple
- set
- list
- union
- dict
@annotated UX improvement: Split between structural and constraint checks.
Minor: Update dev dependencies and reorder CI a bit
v1.3.0
Binding and helpers to hypothesis library for test data generation.
v1.2.0
Full support of annotated-types library for checking PEP-593 compatible type annotations
automatically through the @annotated decorator.
Generated contracted functions are now of a ContractedFunction class, with a original_call
attribute that contains the function without contracts checking.
Pyright check for the totality of the code.
v1.1.0
Contracts can be disabled at runtime with ignore_preconditions() and ignore_postconditions()
Contracts are disabled from the start with python optimized (-O) flag.
Fix a bug where contracts would hide an incorrect function call
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file contractme-1.9.3.tar.gz.
File metadata
- Download URL: contractme-1.9.3.tar.gz
- Upload date:
- Size: 75.3 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee0d57f405a189c4370c789c245c3484cabfead3a2efe52013c70974224d0940
|
|
| MD5 |
99ab5b216d12ec9bfd5f8001feacfcf8
|
|
| BLAKE2b-256 |
cff33f2c6c49201cd14afb74869401b3760f1d7d580acbef420d9fb1dbdea35a
|
File details
Details for the file contractme-1.9.3-py3-none-any.whl.
File metadata
- Download URL: contractme-1.9.3-py3-none-any.whl
- Upload date:
- Size: 44.3 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae47293022b4a2ea2af7949f30ca588b956ce79ef92dd8f54044889835820908
|
|
| MD5 |
af3819ab5f0207da67c001aeaa035733
|
|
| BLAKE2b-256 |
d3ea4f97595851e08b250cf21d3b34b29b0a12fcd5aaab5d6dc90bc6b5f842b9
|