The Problem with Sprawling Logic Business rules often start simple but inevitably mutate into a maintenance nightmare. You might find an `if` statement checking user permissions in an API endpoint, only to see a nearly identical block of code in a reporting script or a CLI tool. This duplication creates a fragile codebase where a policy change requires updates in a dozen disconnected places. If you miss one, you introduce subtle bugs and security holes. The Specification Pattern offers a way out. Originally popularized in Domain-Driven Design, this pattern treats business rules as first-class citizens. Instead of embedding logic directly in control flow, you encapsulate rules into small, reusable building blocks. These blocks can be combined using boolean logic—AND, OR, NOT—to build complex requirements from simple primitives. This approach separates the **definition** of a rule from its **execution**. Prerequisites Before implementing this pattern, you should be comfortable with the following Python concepts: - **Higher-Order Functions**: Understanding functions that take or return other functions. - **Decorators**: Knowledge of how to wrap functions to modify their behavior. - **Generics**: Using `TypeVar` and `Generic` to write type-safe, reusable code. - **Dunder Methods**: Overloading operators like `__and__`, `__or__`, and `__invert__`. Key Libraries & Tools - **typing**: Used for `Callable`, `Generic`, `TypeVar`, and `Any` to ensure strict type hinting. - **functools**: Specifically `wraps`, which is essential for preserving metadata when creating decorators. - **json**: Used in the final stage to demonstrate loading business rules from external configuration files. - SerpApi: A tool mentioned for handling external data retrieval, though not core to the pattern implementation itself. Step 1: Building the Predicate Foundation A predicate is a function that takes an object and returns a boolean. In Python, we can wrap this function in a class that overloads bitwise operators to allow for a natural syntax. This allows us to write `RuleA & RuleB` without immediately evaluating the logic. ```python from typing import Callable, Generic, TypeVar T = TypeVar("T") class Predicate(Generic[T]): def __init__(self, func: Callable[[T], bool]): self.func = func def __call__(self, obj: T) -> bool: return self.func(obj) def __and__(self, other: "Predicate[T]") -> "Predicate[T]": return Predicate(lambda x: self.func(x) and other.func(x)) def __or__(self, other: "Predicate[T]") -> "Predicate[T]": return Predicate(lambda x: self.func(x) or other.func(x)) def __invert__(self) -> "Predicate[T]": return Predicate(lambda x: not self.func(x)) ``` This structure creates a DSL (Domain Specific Language) for rules. The `__call__` method ensures the `Predicate` instance acts like a function, while the logic inside `__and__` and `__or__` creates new combined predicates that stay dormant until called. Step 2: Refining with Decorators and Factories Writing manual lambdas inside the `Predicate` class is tedious. A more Pythonic approach uses a decorator to transform standard functions into `Predicate` objects. By adding a factory layer, we can also pass arguments (like a minimum age or a specific country code) to our rules. ```python from functools import wraps def rule(func: Callable): @wraps(func) def wrapper(*args, **kwargs) -> Predicate: return Predicate(lambda obj: func(*args, obj, **kwargs)) return wrapper @rule def is_active(user) -> bool: return user.active @rule def min_age(age: int, user) -> bool: return user.age >= age Usage: access_rule = is_active() & min_age(18) ``` This refactoring makes the rules highly readable. The `rule` decorator handles the heavy lifting, allowing the developer to focus on the business logic inside the decorated functions. Step 3: From Code to Data The ultimate power of the Specification Pattern lies in its ability to turn logic into data. By registering these rules in a dictionary, we can parse a JSON or YAML file and reconstruct the logic at runtime. This enables non-developers, such as product owners, to adjust thresholds like "Minimum Credit Score" or "Permitted Countries" without touching the source code. ```python rules_registry = {"is_active": is_active, "min_age": min_age} def load_rule(config: dict) -> Predicate: # Recursively build the predicate tree from JSON config # Example logic to parse AND/OR structures omitted for brevity pass ``` Syntax Notes & Practical Examples - **Generic Type T**: By using `Generic[T]`, this pattern isn't limited to "Users." You can use it for `Product` validation, `Order` processing, or `Sensor` data filtering. - **Operator Precedence**: Remember that bitwise operators (`&`, `|`, `~`) have different precedence than logical operators (`and`, `or`, `not`). Use parentheses to ensure rules evaluate in the intended order. Real-world applications include dynamically building database queries based on user filters or creating a flexible permissions system where roles are defined by a combination of fine-grained predicates. Tips & Gotchas - **Overengineering Warning**: This pattern is powerful but introduces complexity. Only use it when business rules are highly volatile or need to be reused across multiple domains. - **Preserving Metadata**: Always use `@functools.wraps` in your decorators. Without it, debugging becomes difficult because the decorated functions lose their original names and docstrings. - **Type Checking**: Python's type system can struggle with deeply nested generic wrappers. Keep your rule factories simple to help tools like Mypy provide accurate feedback.
SerpApi
Companies
- Jan 23, 2026
- Dec 19, 2025