Skip to content

Composable specifications for Python

Turn business rules into objects. Combine them. Reuse everywhere.

Get Started Cookbook


The problem

Business rules scatter across your codebase. A check like "is this order eligible for free shipping?" lives in a service, copied to a view, slightly different in validation. When requirements change, you hunt down every copy.

zspec turns each rule into a single, testable object. Combine them with &, |, ^, ~ to express complex logic without writing new classes.

Five minutes

from dataclasses import dataclass
from zspec import Specification


@dataclass
class Order:
    total: int
    is_paid: bool


class Paid(Specification[Order]):
    def is_satisfied_by(self, order: Order) -> bool:
        return order.is_paid


class MinimumAmount(Specification[Order]):
    def __init__(self, amount: int) -> None:
        self.amount = amount

    def is_satisfied_by(self, order: Order) -> bool:
        return order.total >= self.amount


# Compose with &, |, ~
free_shipping = Paid() & MinimumAmount(500)

order = Order(total=600, is_paid=True)
assert free_shipping(order)

Why zspec?

Composable & \| ^ ~ — build complex rules from simple ones. No new classes needed.
Zero dependencies Standard library only. Optional extras for SQLAlchemy, Django, Polars, and Pandas.
Type-safe Generic Specification[T] preserves candidate types. Full pyrefly strict mode.
Database translators One spec → in-memory check, SQL, MongoDB, Django Q, Polars, or Pandas expression.
Serializable Serialize rules to JSON. Store in configs, databases, or send over API.
Debuggable explain() shows which rules passed and failed — as an ASCII tree.

Operators

Operator Expression Reads as
& a & b Both a AND b
| a | b At least one
^ a ^ b Exactly one
~ ~a NOT a
() spec(c) is_satisfied_by(c)

What else?

# One spec — filter in memory AND query databases
matches = list(eligible.filter(products))
sql = MySql().translate(eligible)
df.filter(MyPolars().translate(eligible))

# Debug why a rule failed
from zspec import explain
print(explain(eligible, order))
# AND FAIL
# ├── Paid PASS
# └── amount >= 500 FAIL

# Build specs from config files
from zspec import from_dict
spec = from_dict(json.loads(config))

# Quick field comparisons — no subclass needed
spec = Specification[Product].matching(price__gte=100, in_stock=True)

Full usage guide Cookbook