Skip to main content

A powerful, expressive and lightweight design-by-contract framework

Project description

ContractMe

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

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 *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
)

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_preconditions and contractme.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

  • Write changelog in README.md
  • Increase version in pyproject.toml
  • uv build
  • Push the resulting new lock file
  • Git tag as v<number>
  • Gitlab will take care of doing the release

Changelog

v1.8.0

  • New contractme.types package: a large, batteries-included library of ready-made Annotated types (numeric, text, paths, network, temporal, identifiers, system, structured-data, containers) usable out of the box with @annotated and 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 / PredicateFactory typing aliases)
  • Factories Bounded[lo, hi] and SuffixPath[".csv", ...], and a SecretStr wrapper
  • Optional extras for richer validators: cron (croniter), iso (pycountry), yaml (pyyaml) — lazily imported, not required to import contractme.types
  • typecheck: nested Annotated constraints (e.g. Annotated[str, LowerCase]) are now flattened, so annotated-types predicate 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


Download files

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

Source Distribution

contractme-1.8.0.tar.gz (62.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-1.8.0-py3-none-any.whl (38.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: contractme-1.8.0.tar.gz
  • Upload date:
  • Size: 62.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for contractme-1.8.0.tar.gz
Algorithm Hash digest
SHA256 2cbe1700218fe21b29b1318b90282621bae8ca2c33eddf0db058ef5760314769
MD5 0f22f57f3a2acf864743f67e9f4e162f
BLAKE2b-256 7343d8a477bc0552c2d7db007f54309a9fe631846f241e732ffae42b8fca0a62

See more details on using hashes here.

File details

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

File metadata

  • Download URL: contractme-1.8.0-py3-none-any.whl
  • Upload date:
  • Size: 38.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for contractme-1.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6c08b4f3ae10602c5633a0b58318c0084926fcbbb99d3d9e7a3ddfbc71c3ef91
MD5 b527bba5ab7dfc99039b43ed3d2f2e1c
BLAKE2b-256 a53fa9c6cfa21e653c2dd226d40dcbc9968c21987d30fa510dec883ef18bbe82

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