Skip to main content

lawcheck

lawcheck logo

Property-based algebraic law testing for Python. Ready-made Hypothesis law suites for semigroups, monoids, commutative structures, groups, abelian groups, semirings, 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

Group, abelian group, and semiring laws

import operator
from hypothesis import strategies as st
from lawcheck import verify_group, verify_abelian_group, verify_semiring

# Group: monoid laws + left and right inverse
verify_group(operator.add, 0, operator.neg, strategy=st.integers(), eq=operator.eq)

# Abelian group: group laws + commutativity (reuses verify_commutative)
verify_abelian_group(
    operator.add, 0, operator.neg, strategy=st.integers(), eq=operator.eq
)

# Semiring: additive commutative monoid + multiplicative monoid +
# distributivity + annihilation by zero
verify_semiring(
    operator.add, operator.mul, 0, 1, strategy=st.integers(), eq=operator.eq
)

# The non-negative integers are a monoid under addition but not a group:
# no function N -> N can satisfy the inverse laws, so the inverse check fails
verify_group(
    operator.add,
    0,
    lambda x: 0,
    strategy=st.integers(min_value=0, max_value=100),
    eq=operator.eq,
)
# Raises: Left inverse violated

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
Group Monoid + left inverse: op(x, inverse(x)) == identity; right inverse: op(inverse(x), x) == identity
Abelian group Group + commutativity
Semiring Additive commutative monoid (add, zero) + multiplicative monoid (mul, one) + left/right distributivity of mul over add + annihilation: mul(zero, x) == zero and mul(x, zero) == zero
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.

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.4.0.tar.gz (888.7 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.4.0-py3-none-any.whl (16.6 kB view details)

Uploaded Python 3

File details

Details for the file lawcheck-0.4.0.tar.gz.

File metadata

  • Download URL: lawcheck-0.4.0.tar.gz
  • Upload date:
  • Size: 888.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for lawcheck-0.4.0.tar.gz
Algorithm Hash digest
SHA256 d329855ccbbb1db8ae7172f065f50ffaab25be2039676d2fe21ffff4e6411b5d
MD5 aa8d13495a343358a2aaa8ab6bac984f
BLAKE2b-256 52939c280a49608c5d7bd06948bba894684ef85f4e05b84d4430e3d6b1edfc2e

See more details on using hashes here.

File details

Details for the file lawcheck-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: lawcheck-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 16.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.13

File hashes

Hashes for lawcheck-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dced50f88a69391ee551329b5d6ead437d264b129df179a6059ee36cd1663722
MD5 ec093baf7da210733543911eee20c8e1
BLAKE2b-256 5520ef93b13f2cf38a1b63e3ec34081eef8b7fdbb67e39a5b322b49721e9ba14

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 Sentry Error logging StatusPage Status page