1# pydantic 2 3pydantic is a library, not the language. but it's become foundational enough that it's worth understanding. 4 5## the core idea 6 7python's type hints don't do anything at runtime by default. `def foo(x: int)` accepts strings, floats, whatever - the annotation is just documentation. 8 9pydantic makes them real. define a model with type hints, and pydantic validates and coerces data to match: 10 11```python 12from pydantic import BaseModel 13 14class User(BaseModel): 15 name: str 16 age: int 17 18user = User(name="alice", age="25") # age coerced to int 19user = User(name="alice", age="not a number") # raises ValidationError 20``` 21 22this is why pydantic shows up everywhere in python - it bridges the gap between python's dynamic runtime and the desire for validated, typed data. 23 24## settings from environment 25 26the most common use: replacing `os.getenv()` calls with validated configuration. 27 28```python 29from pydantic import Field 30from pydantic_settings import BaseSettings, SettingsConfigDict 31 32class Settings(BaseSettings): 33 """settings for atproto cli.""" 34 35 model_config = SettingsConfigDict( 36 env_file=str(Path.cwd() / ".env"), 37 env_file_encoding="utf-8", 38 extra="ignore", 39 case_sensitive=False, 40 ) 41 42 atproto_pds_url: str = Field( 43 default="https://bsky.social", 44 description="PDS URL", 45 ) 46 atproto_handle: str = Field(default="", description="Your atproto handle") 47 atproto_password: str = Field(default="", description="Your atproto app password") 48 49settings = Settings() 50``` 51 52`model_config` controls where settings come from (environment, .env files) and how to handle unknowns. required fields without defaults fail at import time - not later when you try to use them. 53 54from [pdsx/_internal/config.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/config.py) 55 56## annotated types for reusable validation 57 58when multiple schemas share the same validation logic, bind it to the type itself instead of repeating `@field_validator` on each schema: 59 60```python 61from datetime import timedelta 62from typing import Annotated 63from pydantic import AfterValidator, BaseModel 64 65def _validate_non_negative_timedelta(v: timedelta) -> timedelta: 66 if v < timedelta(seconds=0): 67 raise ValueError("timedelta must be non-negative") 68 return v 69 70NonNegativeTimedelta = Annotated[ 71 timedelta, 72 AfterValidator(_validate_non_negative_timedelta) 73] 74 75class RunDeployment(BaseModel): 76 schedule_after: NonNegativeTimedelta 77``` 78 79benefits: 80- write validation once 81- field types become swappable interfaces 82- types are self-documenting 83 84from [coping with python's type system](https://blog.zzstoatzz.io/coping-with-python-type-system/) 85 86## model_validator for side effects 87 88run setup code when settings load: 89 90```python 91from typing import Self 92from pydantic import model_validator 93from pydantic_settings import BaseSettings 94 95class Settings(BaseSettings): 96 debug: bool = False 97 98 @model_validator(mode="after") 99 def configure_logging(self) -> Self: 100 setup_logging(debug=self.debug) 101 return self 102 103settings = Settings() # logging configured on import 104``` 105 106the validator runs after all fields are set. use for side effects that depend on configuration values. 107 108from [bot/config.py](https://github.com/zzstoatzz/bot) 109 110## when to use what 111 112pydantic models are heavier than they look - they do a lot of work on instantiation. for internal data you control, python's `dataclasses` are simpler: 113 114```python 115from dataclasses import dataclass 116 117@dataclass 118class BatchResult: 119 """result of a batch operation.""" 120 successful: list[str] 121 failed: list[tuple[str, Exception]] 122 123 @property 124 def total(self) -> int: 125 return len(self.successful) + len(self.failed) 126``` 127 128no validation, no coercion, just a class with fields. use pydantic at boundaries (API input, config files, external data) where you need validation. use dataclasses for internal data structures. 129 130from [pdsx/_internal/batch.py](https://github.com/zzstoatzz/pdsx/blob/main/src/pdsx/_internal/batch.py) 131 132sources: 133- [how to use pydantic-settings](https://blog.zzstoatzz.io/how-to-use-pydantic-settings/) 134- [coping with python's type system](https://blog.zzstoatzz.io/coping-with-python-type-system/)