Icontract: Design-by-Contract for Python
icontract provides design-by-contract for Python, allowing developers to define preconditions, postconditions, and invariants for functions and classes using expressive lambda functions. It helps ensure correctness and provides informative violation messages, failing fast when contracts are broken. The current version is 2.7.3, and it maintains a steady release cadence with frequent patch updates addressing bugs and adding support for newer Python versions.
Common errors
-
icontract.errors.ViolationError: ...
cause A contract (precondition, postcondition, or invariant) was violated during program execution.fixReview the contract condition that failed and the input values or object state that led to its violation. This often indicates a bug in the calling code or an incorrect assumption about the function's behavior. -
NameError: name 'old' is not defined
cause You are trying to use `old` in an `@ensure` contract without declaring it as an argument in your lambda function.fixModify your `@ensure` lambda to accept `old` as its first argument (e.g., `lambda old, result: ...` for a function returning a value, or `lambda old, self: ...` for a method to access the state before the call). -
TypeError: <lambda>() missing 1 required positional argument: 'result'
cause Your `@ensure` contract lambda expects `result` (the return value of the function) but the decorated function is `None` or an argument is missing.fixEnsure the lambda signature for `@ensure` correctly matches the function's return type. If the function has no return value, use `lambda old, self: ...` or simply `lambda self: ...` for methods, or `lambda: ...` for functions without arguments. -
TypeError: <lambda>() takes 1 positional argument but 2 were given
cause Your contract lambda's signature does not match the arguments provided by `icontract` (e.g., expecting only `self` but also receiving `old` or `result`).fixAdjust the lambda signature to accept all arguments `icontract` provides for that context (e.g., `lambda self: ...` for a simple invariant, `lambda self, old: ...` for invariant checking `old` state, `lambda x: ...` for a precondition on argument `x`).
Warnings
- gotcha Invariants on `super().__init__` calls in child classes were incorrectly checked in versions prior to 2.7.2, potentially leading to false negatives or unexpected behavior in inheritance hierarchies.
- gotcha A bug in version 2.7.0 caused invariants defined on derived classes to incorrectly 'leak' up to parent classes, leading to unintended invariant checks on base class instances or methods.
- gotcha By default, invariants are checked on method calls. As of 2.7.0, `icontract` allows enforcing invariants on attribute setting (`__setattr__`). If enabled via `enforce_on_setattr=True`, this can introduce significant performance overhead or unexpected `ViolationError`s if not carefully managed.
- gotcha When defining postconditions (`@ensure`), if you need to compare the state after the function execution with the state *before*, you must explicitly include `old` as an argument in your lambda expression (e.g., `lambda old, result: ...`). Failing to do so will result in a `NameError`.
Install
-
pip install icontract
Imports
- require
from icontract import require
- ensure
from icontract import ensure
- invariant
from icontract import invariant
- ViolationError
from icontract.errors import ViolationError
from icontract import ViolationError
Quickstart
import icontract
@icontract.require(lambda x: x > 0, "Input must be positive")
@icontract.ensure(lambda result: result > 0, "Result must be positive")
def double(x: int) -> int:
return x * 2
@icontract.invariant(lambda self: self.value >= 0, "Value must be non-negative")
class Counter:
def __init__(self, initial_value: int) -> None:
self.value = initial_value
@icontract.ensure(lambda old, self: self.value == old.value + 1)
def increment(self) -> None:
self.value += 1
# Example usage
try:
print(f"Doubling 5: {double(5)}")
# This will raise a ViolationError
# double(-1)
except icontract.ViolationError as e:
print(f"Contract violation caught: {e}")
c = Counter(0)
print(f"Initial counter value: {c.value}")
c.increment()
print(f"Counter after increment: {c.value}")
try:
# This would violate the invariant if direct assignment was not restricted
# c.value = -5
# To see invariant violation on setattr, 'enforce_on_setattr' must be set
pass
except icontract.ViolationError as e:
print(f"Invariant violation caught: {e}")