Skip to main content

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

lawcheck logo

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


Download files

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

Source Distribution

lawcheck-0.3.0.tar.gz (884.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lawcheck-0.3.0-py3-none-any.whl (14.9 kB view details)

Uploaded Python 3

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

Hashes for lawcheck-0.3.0.tar.gz
Algorithm Hash digest
SHA256 29d39e8b15911beba8449b7af3d5d1609eb0a67e0bef7a33467d97fc1e73e1cf
MD5 20ebaf1e0e7be4e742f6f6d6fca0a40c
BLAKE2b-256 5388c60fa0c773013218819f33a89cf333c2b611e199bc55503cec5b7a0c4d50

See more details on using hashes here.

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

Hashes for lawcheck-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 085de93cd1eb77c4c21abfdba60ae5f6e462b48c9d2609dda28347b5c8e6f33e
MD5 0d9548188c06b49b8c774fda2263d566
BLAKE2b-256 d4b10c26f17c9c4b195640493d93c4deb56331932458d97b3fd000a13a13d93d

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