Skip to main content

Property-based algebraic law testing for Python: Hypothesis-driven law suites for semigroups, monoids, functors, applicatives, and monads, 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, and monads, 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,
)

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

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.2.0.tar.gz (880.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.2.0-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: lawcheck-0.2.0.tar.gz
  • Upload date:
  • Size: 880.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for lawcheck-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d3ad601ff3b1c70cbd695d8f922b9073953c09cc55d96d373477b1a1850b8f76
MD5 0ccfb9ab2a907b1800aa0bc7fcbc8197
BLAKE2b-256 f8f36f27ea0b0af716af778fa5d11a235edd5b21a581cd9e55459fef8b35fa82

See more details on using hashes here.

File details

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

File metadata

  • Download URL: lawcheck-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.0 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88f4367c29a0471084366762ae9d04248e1fe6274c5ab5822af976ba1acd418f
MD5 778b574140ea51a4c42b8f95519aad8d
BLAKE2b-256 0cb155c454ee38afac4b478449b4328b947b47e004dead555805b0800d7af489

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