Checkable contracts for python
Project description
Pactum
Pactum is a Python library designed to help both library implementers and their users to write correct code. It provides decorators for adding contract annotations, serving multiple purposes:
- Documentation
Contracts provide clear and precise specification using Python expressions instead of comments. - Runtime Verification
Contracts enable runtime verification, helping catch bugs early in the development process. - Static Analysis
Contracts can also be used for static analysis, although this would require additional implementation effort from third-party tools, which is unlikely to happen.
Overview
Contract assertions are a mechanism to specify and potentially enforce conditions that must hold at certain points of the program. Their key components are:
- Predicate
This is the condition that must be true for the assertion to hold. - Bound Variables
This is the set of variables accessible to the predicate, e.g. function arguments. - Evaluation Semantic
The evaluation semantic determines whether the assertion is evaluated at runtime, or not. - Labels
Labels can be attached to contract assertions to influence the effective evaluation semantic for that assertion.
Pactum knows two kinds of contract assertions:
- Preconditions
Defined using the@predecorator, indicates that a condition must hold before a function is executed or a scope is entered. - Postconditions
Defined using the@postdecorator, indicates that a condition must hold after a function executed or a scope is left.
Usage Example
@pre(lambda ip: is_valid_ip(ip))
@pre(lambda port: is_valid_port(port))
@post(lambda result: is_valid_http_status(result))
def get_from_network(ip: str, port: int) -> int:
"""Opens a network connection and returns the HTTP status code"""
return connect_impl(ip, port) # does the actual work
Contract Predicates
A contract predicate is a bool-returning callable. In its simplest form, a predicate has no
parameters. However, it is often useful for a predicate to be able to access surrounding variables.
To that end, predicates can accept arguments.
Predicate Parameters
Predicate parameters must satisfy the following constraints:
- they must be of kind POSITIONAL_OR_KEYWORD (i.e. the declaration must not contain
*or/parameters) [^1] - their name must be in the set of visible bindings
[^1]: This precludes variadic parameters such as *args and **kwargs.
Predicate Bindings
Variables can be explicitly bound to make them available in the predicate. For example, in preconditions, the function arguments are implicitly bound, and can therefore be accessed by default, simply by giving the predicate parameters of the same name as the decorated function parameter.
Other variables can be bound explicitly by using the capture and clone family of assertion
parameters. Captured variables reference the same object, which means it could change between the
moment it was captured and the predicate is evaluated. Cloned variables on the other hand are deep
copied, and are therefore guaranteed to remain unchanged.
For example, it is possible to access local and/or global variables:
# ...
answer = 1 # partial result
# We know the actual answer is greater than the partial answer previously computed
@post(
lambda result, old_answer: old_answer < result,
capture_before={"old_answer": "answer"}, # capture answer before modification
)
def compute_answer():
"""This is going to take a while"""
global answer
answer = 42
return answer # Really long computation
Evaluation Semantics
By default, all contract assertions are runtime-evaluated. Pactum offers a range of knobs to change that behavior.
Global Evaluation Semantic
At the outermost level, Pactum offers a global switch:
# Turn off all (?) runtime checking
set_contract_evaluation_semantic(EvaluationSemantic.ignore)
Labels
Contract assertions can have labels which can affect the effective evaluation semantic for that
assertion. Labels are attached to assertions with the labels keyword argument, for example:
@pre(lambda: predicate(), labels=[labels.expensive])
Currently, the library comes with two labels that make sense on assertions:
- expensive
This label is supposed to mark very expensive contract checks, and allows turning those off separately. - ignore
This label unconditionally turns of this contract assertion. This might be useful during development to temporarily silence flaky assertions.
Additionally, there is a configurable global label, which affects all contract assertions. The library currently ships two labels meant for use as global label:
- ignore_postconditions
This label turns off all postconditions. - filter_by_module
Technically a label factory, this allows to only turn on assertions in specific modules.
For example:
set_global_contract_assertion_label(labels.filter_by_module("mymodule.*"))
This graph shows the steps taken to determine the effective semantic:
flowchart TD
subgraph global["Global"]
global_semantic["Evaluation Semantic"]
global_label["Label"]
end
subgraph local["Contract Assertion-Local"]
label1["Label 1"]
labelx["..."]
labeln["Label n"]
effective_semantic["Effective Evaluation Semantic"]
end
global_semantic --> global_label
global_label --> label1
label1 --> labelx
labelx --> labeln
labeln --> effective_semantic
Contract Kinds
Preconditions
Preconditions are expected to hold before the function/scope is entered. Since it's so common (and safe), function arguments are implicitly bound to preconditions.
| Decorator | Context manager | |
|---|---|---|
| Function arguments | Implicitly captured, capture={...}, clone={...} |
❌ |
| Return value | ❌ | ❌ |
| Variables before entering | capture={...}, clone={...} |
capture={...}, clone={...} |
| Labels | labels=[...] |
labels=[...] |
Example:
with pre(lambda a, b: a == b, capture={"a": "foo"}, clone={"b": "bar"}, labels=[labels.expensive]):
"""In here, a==b, where a refers to some outer variable called foo, and b is a deep copy of
variable "bar". This precondition is marked as expensive."""
Postconditions
Postconditions are expected to hold after the function/scope exits. Since it is very common to access the function's return value in a postcondition, the first parameter not otherwise bound is assumed to refer to the return value.
| Decorator | Context manager | |
|---|---|---|
| Function arguments | capture_before={...}, clone_before={...} |
❌ |
| Return value | first unbound parameter | ❌ |
| Variables before entering | capture_before={...}, clone_before={...} |
capture_before={...}, clone_before={...} |
| Variables after exiting | capture_after={...}, clone_after={...} |
capture_after={...}, clone_after={...} |
| Labels | labels=[...] |
labels=[...] |
References
PEP-0316 Programming by Contract for Python
This PEP wants to add contracts to the language. It has been around since 2003. I don't know what
its status is.
P2900 Contracts for C++
A proposal to add contracts to C++. I took some inspiration from there.
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 pypactum-0.3.2.tar.gz.
File metadata
- Download URL: pypactum-0.3.2.tar.gz
- Upload date:
- Size: 19.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
28841abec28368bee22bba1ebaede5b9e1e7f9752e37524d0eb199d2ac87e826
|
|
| MD5 |
4d5c921ab431be99ee07569861048df0
|
|
| BLAKE2b-256 |
d83da993a55683ba2e9558f4b1b4973edf57127eb0855a38a006f7bef406baf8
|
Provenance
The following attestation bundles were made for pypactum-0.3.2.tar.gz:
Publisher:
python-package.yml on jan-moeller/pactum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypactum-0.3.2.tar.gz -
Subject digest:
28841abec28368bee22bba1ebaede5b9e1e7f9752e37524d0eb199d2ac87e826 - Sigstore transparency entry: 179124482
- Sigstore integration time:
-
Permalink:
jan-moeller/pactum@7a90347832fb58d9d372f0fc42c1bd517b3e40a9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jan-moeller
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-package.yml@7a90347832fb58d9d372f0fc42c1bd517b3e40a9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pypactum-0.3.2-py3-none-any.whl.
File metadata
- Download URL: pypactum-0.3.2-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
014c5349b6769b082c250ef101905cb6566eee0b2299029de2655c7507085329
|
|
| MD5 |
b28c88d4d70227ea42780c75c319d949
|
|
| BLAKE2b-256 |
44819042f3e2f40bb9417166828d2124f219b5da8254873ee215d1b8ba2e9bff
|
Provenance
The following attestation bundles were made for pypactum-0.3.2-py3-none-any.whl:
Publisher:
python-package.yml on jan-moeller/pactum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pypactum-0.3.2-py3-none-any.whl -
Subject digest:
014c5349b6769b082c250ef101905cb6566eee0b2299029de2655c7507085329 - Sigstore transparency entry: 179124487
- Sigstore integration time:
-
Permalink:
jan-moeller/pactum@7a90347832fb58d9d372f0fc42c1bd517b3e40a9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/jan-moeller
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-package.yml@7a90347832fb58d9d372f0fc42c1bd517b3e40a9 -
Trigger Event:
push
-
Statement type: