about things
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/)