Property-based algebraic law testing for Python: Hypothesis-driven law suites for semigroups, monoids, functors, applicatives, monads, equivalence relations, and total orders, with a pluggable equality oracle.
Project description
lawcheck
Property-based algebraic law testing for Python. Ready-made Hypothesis law suites for semigroups, monoids, commutative structures, functors, applicatives, monads, equivalence relations (Eq), and total orders (Ord), with a pluggable equality oracle.
The problem
Algebraic laws (associativity, identity, functor composition, monad associativity) are easy to state but hard to test systematically. You want Hypothesis to search for counterexamples, not to write the laws yourself every time.
The bigger problem: what does "equal" mean for your type?
For int it's obvious. For IO[A], Task, async wrappers, or any effectful type, two logically equal values are often physically distinct objects. Python's == is wrong or absent. The cats-effect Scala library confronted this exactly and introduced a formal Eq type-class hierarchy. lawcheck asks you to supply eq explicitly so the meaning of "equal" is always unambiguous.
Install
pip install lawcheck
Sole runtime dependency: hypothesis.
Usage
Layer 1: pure primitives (no Hypothesis, deterministic)
import operator
from lawcheck import holds_associative, assert_associative
# Check one triple
holds_associative(operator.add, 1, 2, 3, eq=operator.eq) # True
holds_associative(operator.sub, 1, 2, 3, eq=operator.eq) # False
# Assert or raise with a precise message
assert_associative(operator.sub, 1, 2, 3, eq=operator.eq)
# AssertionError: Associativity violated: (op(1, 2)) op 3 = -4, but 1 op (op(2, 3)) = 2
Layer 2: Hypothesis runners (search for counterexamples)
import operator
from hypothesis import strategies as st
from lawcheck import verify_monoid, verify_commutative, verify_semigroup
# Passes for int addition (associative, left and right identity at 0, commutative)
verify_monoid(operator.add, 0, strategy=st.integers(), eq=operator.eq)
verify_commutative(operator.add, strategy=st.integers(), eq=operator.eq)
# Finds a counterexample for subtraction
verify_semigroup(operator.sub, strategy=st.integers(), eq=operator.eq)
# Raises with Hypothesis's shrunk minimal counterexample
Functor, applicative, and monad laws
from lawcheck import verify_functor, verify_applicative, verify_monad
# fmap: (f: A -> B, fa: F[A]) -> F[B]
verify_functor(
my_fmap,
f=lambda x: x * 2,
g=lambda x: x + 1,
strategy=my_strategy,
eq=my_eq,
)
# pure: A -> F[A]; ap: (F[A -> B], F[A]) -> F[B]
verify_applicative(
my_pure,
my_ap,
lambda x: x * 2,
value_strategy=st.integers(),
functor_strategy=my_fa_strategy,
ap_strategy=my_fn_strategy,
eq=my_eq,
)
verify_monad(
ret=my_return,
bind=my_bind,
f_arrow=lambda x: my_return(x + 1),
g_arrow=lambda x: my_return(x * 2),
strategy=st.integers(),
monad_strategy=my_monad_strategy,
eq=my_eq,
)
Eq and Ord laws
import operator
from hypothesis import strategies as st
from lawcheck import verify_eq, verify_order
# Eq: reflexivity, symmetry, transitivity of an equality predicate
verify_eq(operator.eq, strategy=st.integers())
# A broken relation (equal if within 1) is reflexive and symmetric but not
# transitive, so verify_eq finds a counterexample:
verify_eq(lambda a, b: abs(a - b) <= 1, strategy=st.integers(min_value=0, max_value=10))
# Raises: Eq transitivity violated
# Ord: reflexivity, antisymmetry (up to eq), transitivity, totality of a <=
verify_order(operator.le, strategy=st.integers(), eq=operator.eq)
# Divisibility is only a partial order; 2 and 3 are incomparable, so totality fails:
verify_order(lambda a, b: b % a == 0, strategy=st.integers(min_value=2, max_value=12), eq=operator.eq)
# Raises: Order totality violated
Pluggable equality oracle
from lawcheck import normalized_eq, lifted_eq, value_eq
# Compare after normalization (e.g. abs value, canonical form)
abs_eq = normalized_eq(abs)
# Unwrap a container and compare inner values
box_eq = lifted_eq(lambda b: b.value, operator.eq)
# Plain == (correct for ints, strings, lists)
value_eq(1, 1) # True
Laws implemented
| Structure | Laws |
|---|---|
| Semigroup | Associativity: (a op b) op c == a op (b op c) |
| Monoid | Associativity + left identity + right identity |
| Commutative | a op b == b op a |
| Functor | Identity: fmap(id) == id; Composition: fmap(f.g) == fmap(f).fmap(g) |
| Applicative | Identity: ap(pure(id), v) == v; Homomorphism: ap(pure(f), pure(x)) == pure(f(x)); Interchange: ap(u, pure(y)) == ap(pure(lambda f: f(y)), u); Composition: ap(ap(ap(pure(compose), u), v), w) == ap(u, ap(v, w)) |
| Monad | Left identity, right identity, associativity of bind |
| Eq | Reflexivity: eq(x, x); Symmetry: eq(x, y) == eq(y, x); Transitivity: eq(x, y) and eq(y, z) implies eq(x, z) |
| Ord | Reflexivity: le(x, x); Antisymmetry: le(x, y) and le(y, x) implies eq(x, y); Transitivity: le(x, y) and le(y, z) implies le(x, z); Totality: le(x, y) or le(y, x) |
The eq parameter is required everywhere
lawcheck has no default for eq. Where ergonomics would tempt a default, there is an explicit second function instead (e.g., value_eq for plain types). This is by design: the cats-effect equality-oracle problem is real, and baking in == would silently produce wrong answers for effects, async types, and custom containers.
See docs/architecture.md for the full design rationale.
Contributing
See CONTRIBUTING.md and CODE_OF_CONDUCT.md.
License
MIT. See LICENSE.
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 lawcheck-0.3.0.tar.gz.
File metadata
- Download URL: lawcheck-0.3.0.tar.gz
- Upload date:
- Size: 884.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29d39e8b15911beba8449b7af3d5d1609eb0a67e0bef7a33467d97fc1e73e1cf
|
|
| MD5 |
20ebaf1e0e7be4e742f6f6d6fca0a40c
|
|
| BLAKE2b-256 |
5388c60fa0c773013218819f33a89cf333c2b611e199bc55503cec5b7a0c4d50
|
File details
Details for the file lawcheck-0.3.0-py3-none-any.whl.
File metadata
- Download URL: lawcheck-0.3.0-py3-none-any.whl
- Upload date:
- Size: 14.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
085de93cd1eb77c4c21abfdba60ae5f6e462b48c9d2609dda28347b5c8e6f33e
|
|
| MD5 |
0d9548188c06b49b8c774fda2263d566
|
|
| BLAKE2b-256 |
d4b10c26f17c9c4b195640493d93c4deb56331932458d97b3fd000a13a13d93d
|