Composable specifications for Python
Turn business rules into objects. Combine them. Reuse everywhere.
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)