Good Python schemas specify interfaces and protocols without specifying implementation. They leverage type-hinting and linting tools like pylint and mypy.
-
Early Design Documentation: Define comprehensive interface specifications before writing implementation code using Python's
abcmodule, strict type annotations, detailed docstrings, and frozen Pydantic models. This creates a clear blueprint with static validation viamypy. -
Guardrails for LLM Code Generation: Explicit interfaces, type hints, immutable data models, and runtime validation via Pydantic's
validate_callreduce ambiguity and minimize LLM hallucinations or structural deviations. -
Simplified Testing: Strict separation between abstract interfaces and concrete implementations naturally promotes dependency injection, making it straightforward to substitute mocks during testing.
- Interface Classes: Use
abc.ABCwith abstract methods, full type annotations, and comprehensive docstrings. Prefix interface names with "I" for clarity. - No Implementation Logic: Interfaces contain only method signatures and documentation.
- Frozen Pydantic Models: Data models use
BaseModelwithfrozen=Truefor immutability. - Pydantic Field Annotations: Use
Field()for inline documentation, examples, and validation constraints (ranges, patterns, lengths). This serves both as developer documentation and as guardrails. - Runtime Validation: All functions use
@pydantic.validate_callto enforce type hints at runtime. - Default Returns: Schema functions return default values of correct type as placeholders.
- Static Validation: Run
mypyto ensure type consistency.
from abc import ABC, abstractmethod
from pydantic import BaseModel, ConfigDict, Field, validate_call
class User(BaseModel):
model_config = ConfigDict(frozen=True)
id: int = Field(
...,
gt=0,
description="Unique user identifier",
examples=[1, 42, 1337]
)
name: str = Field(
...,
min_length=1,
max_length=100,
description="User's display name",
examples=["Alice", "Bob Smith"]
)
default_user = User(id=1, name="")
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.
"""
return default_user # Placeholder
@validate_call
def calculate_discount(price: float, percentage: float) -> float:
"""Calculate discounted price."""
return 0.0 # PlaceholderSchemas can extend other schemas, and implementations can extend other implementations, maintaining the abstraction-to-concrete relationship at each level.
Example: In a knowledge graph framework, a base schema defines domain-agnostic operations (document ingestion, entity extraction, graph construction). A medical literature schema extends this base, adding domain-specific concepts like evidence strength, claim validation, and citation networks. Both schemas have corresponding implementations that maintain the same inheritance structure:
IKnowledgeGraphSchema→IMedicalLiteratureSchema(schema layer)KnowledgeGraphImpl→MedicalLiteratureImpl(implementation layer)
This parallelogram structure keeps domain logic separated while preserving type safety and testability at both abstraction levels.
- Keep ABC and Pydantic Separate: Mixing abstract base classes with Pydantic models adds complexity around schema generation and serialization. Use Pydantic for pure data structures, ABC for functional interfaces.
- Immutability Caveat:
frozen=Trueprevents attribute reassignment but doesn't deeply freeze mutable fields (lists/dicts inside models remain mutable). - When to Use: Best for codebases requiring strict contracts and validation—financial, healthcare, mission-critical applications.