Immutable Interface Design (IID) is a proposed architectural pattern for Python development. The primary goals of this protocol are threefold:
- Early and Specific Design Documentation: IID emphasizes defining comprehensive interface specifications before writing implementation code. This is achieved through the use of Python's
abc
module for interface classes, abstract methods with strict type annotations, detailed docstrings for all components, and frozen Pydantic models for immutable data structures12. This approach creates a clear blueprint, ensuring design details are captured early in the development process34. Static validation with tools likemypy
further enforces type consistency from the outset. - Robust Guardrails for LLM Code Generation: The detailed and validated structure provided by IID serves as effective guardrails when using Large Language Models (LLMs) for code generation56. By providing explicit interfaces, type hints, immutable data models, and runtime validation via Pydantic's
validate_call
, the protocol significantly reduces ambiguity7. This minimizes the risk of common LLM issues such as hallucinations, generating incorrect structures, or producing code that deviates from the intended design5689. - Simplified Testing through Dependency Injection: IID naturally promotes a design centered around dependency injection (DI) by enforcing a strict separation between abstract interfaces and their concrete implementations1011. Defining dependencies through interfaces makes it straightforward to substitute mock or stub objects during unit testing, simplifying test setup and improving the overall testability of the codebase12131415.
Immutable Interface Design (IID) is characterized by the following elements:
- Interface Classes with abc and Abstract Methods: All interfaces are defined using Python’s
abc
module, ensuring that any subclass must implement the specified abstract methods. These interfaces are strictly type-annotated and include comprehensive docstrings for every method and class161718. - No Implementations in Interfaces: The interface classes contain no business logic—only method signatures and documentation. Actual logic is provided later in concrete subclasses or implementations1617.
- Frozen Pydantic Models: Data models are defined using Pydantic’s
BaseModel
with thefrozen
configuration, making instances immutable—attempts to modify fields after instantiation will raise errors19. This ensures data integrity throughout the system. - Interface names start with "I": Not strictly necessary but it makes the code a teeny bit more readable.
- Pydantic
validate_call
Decorator: All functions in the IID are wrapped withpydantic.validate_call
, which enforces runtime validation of input arguments (and optionally return values) against their type hints20. Initially, these functions return a default value of the correct type, serving as a placeholder until actual implementations are provided. - Comprehensive Typing and Docstrings: Every class, method, and function includes explicit type hints and detailed docstrings, promoting clarity and maintainability.
- mypy Static Validation: The entire design is statically validated using
mypy
to ensure type consistency, both at the interface definition stage and after concrete implementations are provided.
from abc import ABC, abstractmethod
from pydantic import BaseModel, validate_call, ConfigDict
class UserModel(BaseModel):
model_config = ConfigDict(frozen=True)
id: int
name: str
default_user = UserModel(id=0, name="")
class IUserServiceInterface(ABC):
"""
Interface for user service operations.
"""
@abstractmethod
@validate_call
def get_user(self, user_id: int) -> UserModel:
"""
Retrieve a user by ID.
Args:
user_id (int): The ID of the user.
Returns:
UserModel: The user data.
"""
# Placeholder: return a default value of correct type
return default_user # To be replaced in implementation
- Combining abc and Pydantic: While it is technically possible to combine abstract base classes (
abc.ABC
) with Pydantic models, this introduces complexity and potential runtime issues, especially regarding schema generation and serialization161718. Careful design is needed to avoid pitfalls such as multiple inheritance complications and Pydantic’s handling of abstract models. - Immutability Caveats: Pydantic’s
frozen
models prevent attribute reassignment, but do not deeply freeze mutable fields (e.g., dicts or lists inside the model remain mutable)19. - Type Enforcement: Using
mypy
ensures that type hints are respected throughout the codebase, catching inconsistencies before runtime.
Feature | Description |
---|---|
Interface Definition | abc-based classes, abstract methods, type hints, docstrings |
Data Models | Pydantic BaseModel with frozen=True for immutability |
Function Validation | @pydantic.validate_call for runtime argument (and return) validation |
Default Return Values | Functions return default values of correct type until implemented |
Static Type Checking | mypy ensures type consistency across interfaces and implementations |
Documentation | Docstrings everywhere for clarity and maintainability |
- IID provides a robust framework for defining clear, immutable, and type-safe interfaces in Python.
- It leverages both static (mypy) and dynamic (Pydantic) validation, promoting correctness and maintainability.
- Developers should be cautious when mixing abc and Pydantic, as not all features are fully compatible, and careful design is required to avoid schema and inheritance issues161718.
This pattern is especially suitable for large codebases where strict contracts, immutability, and validation are critical, such as in financial, healthcare, or mission-critical applications.
Below is a concise IID example featuring:
- One interface class (using
abc.ABC
) - Two frozen Pydantic models
- One standalone function decorated with
pydantic.validate_call
from abc import ABC, abstractmethod
from pydantic import BaseModel, ConfigDict, validate_call
class User(BaseModel):
model_config = ConfigDict(frozen=True)
id: int
name: str
default_user = User(0, "")
class Product(BaseModel):
model_config = ConfigDict(frozen=True)
sku: str
price: float
class IUserRepository(ABC):
"""
Interface for user repository operations.
"""
@abstractmethod
@validate_call
def get_user(self, user_id: int) -> User:
"""
Retrieve a user by their unique ID.
Args:
user_id (int): The user's unique identifier.
Returns:
User: The user object.
"""
# Placeholder: return a default value of correct type
return default_user # To be replaced in implementation
# Standalone function with validate_call
@validate_call
def calculate_discount(product: Product, percentage: float) -> float:
"""
Calculate the discounted price for a product.
Args:
product (Product): The product to discount.
percentage (float): Discount percentage (0-100).
Returns:
float: The discounted price.
"""
# Placeholder: return a default value of correct type
return 0.0 # To be replaced in implementation
Notes:
- All models are immutable (
frozen=True
). - The interface method and standalone function use
@validate_call
for runtime argument validation. - Docstrings and type hints are present everywhere, as per IID principles.
- Default return values are provided as placeholders, to be replaced in concrete implementations.
Footnotes
-
https://www.appacademy.io/blog/python-coding-best-practices ↩
-
https://www.docuwriter.ai/posts/python-documentation-best-practices-guide-modern-teams ↩
-
https://swimm.io/learn/code-documentation/documentation-in-python-methods-and-best-practices ↩
-
https://www.confident-ai.com/blog/llm-guardrails-the-ultimate-guide-to-safeguard-llm-systems ↩ ↩2
-
https://www.animaapp.com/blog/genai/guard-rails-for-llms/ ↩ ↩2
-
https://aws.amazon.com/blogs/machine-learning/build-safe-and-responsible-generative-ai-applications-with-guardrails/ ↩
-
https://xygeni.io/blog/python-dependency-injection-how-to-do-it-safely/ ↩
-
https://arjancodes.com/blog/python-dependency-injection-best-practices/ ↩
-
https://www.reddit.com/r/Python/comments/195uk6d/do_you_prefer_mock_or_dependency_injection_when/ ↩
-
https://stackoverflow.com/questions/33349025/how-to-do-basic-dependency-injection-in-python-for-mocking-testing-purposes ↩
-
https://stackoverflow.com/questions/70884344/how-to-create-a-python-abc-interface-pattern-using-pydantic ↩ ↩2 ↩3 ↩4
-
https://stackoverflow.com/questions/78765650/is-it-possible-or-recommended-to-mix-abstract-base-classes-with-pydantic-base ↩ ↩2 ↩3 ↩4
-
https://github.com/pydantic/pydantic/discussions/4208 ↩ ↩2 ↩3