Cookbook
Filter a collection
Without zspec — inline condition, not reusable:
With zspec — one spec, used everywhere:
Works on generators too — memory-efficient for large datasets:
Build rules from config
Store business rules in JSON. Add new rules without touching code:
import json
from zspec import to_dict, from_dict
# Save
with open("rules.json", "w") as f:
json.dump(to_dict(InStock() & MinPrice(100)), f)
# Load — weeks later
data = json.load(open("rules.json"))
spec = from_dict(data)
results = list(spec.filter(products))
Conditional composition
Build a spec dynamically based on user input:
spec = Specification.true()
if filters.get("min_price"):
spec = spec & MinPrice(filters["min_price"])
if filters.get("in_stock_only"):
spec = spec & InStock()
results = list(spec.filter(products))
spec.true() is a neutral starting point — spec & A when spec is true() just returns A.
Debug a failing rule
Use explain() to see WHY a candidate was rejected:
from zspec import explain
print(explain(eligible, product))
# AND FAIL
# ├── InStock PASS
# └── price >= 100 FAIL
Or visualize the spec structure itself:
Validate in a service layer
Raise an error with a readable message when business rules fail:
def ship(order: Order) -> None:
if not eligible(order):
raise ValueError(f"Order not eligible:\n{explain(eligible, order)}")
# ... proceed with shipping
Translate to database queries
One spec — filter in memory AND in the database. Translators produce filter fragments, you control joins and projections:
# In-memory
eligible.is_satisfied_by(product)
# Same spec → SQL WHERE clause
fragment = SqlTranslator().translate(eligible)
cursor.execute(
f"SELECT * FROM products WHERE {fragment.sql}",
fragment.params,
)
# Same spec → MongoDB $match
collection.find(MongoTranslator().translate(eligible))
# Same spec → Django Q
Product.objects.filter(DjangoQTranslator().translate(eligible))
# Same spec → Polars / Pandas filter
df.filter(PolarsTranslator().translate(eligible))
df.query(PandasTranslator().translate(eligible))
Negate a set of rules
Exclude anything that matches, accept everything else:
# Accept all products EXCEPT those that are too expensive and out of stock
valid = Specification[Product].excluding(
price__gt=1000,
in_stock=False,
)
Quick field comparisons
Skip the subclass boilerplate for simple attribute checks:
# Instead of writing a MinPrice class
eligible = Specification[Product].matching(
price__gte=100, # price >= 100
in_stock=True, # in_stock == True
)
# With field proxies for type-safe comparisons
F = fields(Product)
eligible = Specification[Product].matching(
F.price >= 100,
F.in_stock == True,
)
# With lambda predicates
eligible = Specification[Product].matching(
lambda p: p.price >= 100,
lambda p: p.in_stock,
)
Equality and deduplication
Specs support == and hash — use them in sets and dicts: