Skip to content

Latest commit

 

History

History
117 lines (83 loc) · 3.25 KB

File metadata and controls

117 lines (83 loc) · 3.25 KB

Model Inheritance and Pydantic Validation

Date: 2024-12-07 Topic: Refactoring data models for better code reuse


Background

Today I refactored the health food compliance system's data models, migrating from BaseResponse to BaseJudgement as the foundation class.


The Refactoring Goal

The original structure had too much duplication. Each compliance check repeated the same fields. The new structure uses inheritance:

# Before: Duplicate fields in each model
class DiseaseFunctionCheck(BaseModel):
    is_compliant: bool
    reason: str
    reference: str
    disease_claims: List[str]

class NonPharmaceuticalCheck(BaseModel):
    is_compliant: bool
    reason: str
    reference: str
    statement_found: bool
# After: Shared base class
class BaseJudgement(BaseModel):
    is_or_not: bool = Field(..., description="Compliance status")
    reason: str = Field(..., description="Reasoning")
    reference: str = Field(..., description="Regulatory reference")

class DiseaseFunctionCheck(BaseJudgement):
    disease_claims: List[str] = Field(default_factory=list)

class NonPharmaceuticalCheck(BaseJudgement):
    statement_found: bool

Pydantic Validators

Custom validation logic with decorators:

class DiseaseFunctionCheck(BaseJudgement):
    disease_claims: List[str] = Field(default_factory=list)

    @validator('is_or_not')
    def validate_consistency(cls, v, values):
        # Can't be compliant if disease claims exist
        if 'disease_claims' in values and values['disease_claims'] and v:
            raise ValueError("Inconsistent: disease claims found but marked compliant")
        return v

Composite Result Model

The final result combines all checks:

class HealthFoodAnalysisResult(BaseModel):
    disease_check: DiseaseFunctionCheck
    declaration_check: MedicineDeclarationCheck
    identification_check: IdentificationCheck
    overall_compliance: bool

    @validator('overall_compliance', always=True)
    def calculate_overall(cls, v, values):
        return all([
            values.get('disease_check', {}).is_or_not,
            values.get('declaration_check', {}).is_or_not,
            values.get('identification_check', {}).is_or_not
        ])

FastAPI + Pydantic Stack

FastAPI
├── Starlette (ASGI framework)
├── Pydantic (Data validation)
└── Python Type Hints (Type system)

This stack provides automatic request validation, response serialization, and API documentation.


Today's Reflection

The refactoring was worthwhile. The new structure reduced code by about 30% and made the relationships between models clearer.

The key insight: inheritance is for "is-a" relationships, composition for "has-a". Each compliance check "is a" judgment (inherits BaseJudgement), while the final result "has" multiple checks (composition).

Pydantic's validators are powerful but can be confusing. The values parameter only contains fields that come before the validated field in the class definition. I had to reorder fields to make validators work correctly.


Further Learning

  • Pydantic v2 changes (model_validator, field_validator)
  • Python dataclasses vs Pydantic
  • OpenAPI schema generation
  • Generic types in Pydantic