qqmusic-api-python 0.4.1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. qqmusic_api_python-0.5.0/.agents/skills/pydantic/SKILL.md +1439 -0
  2. qqmusic_api_python-0.5.0/.agents/skills/python-standards/SKILL.md +282 -0
  3. qqmusic_api_python-0.5.0/.agents/skills/tarsio/SKILL.md +247 -0
  4. qqmusic_api_python-0.5.0/.agents/skills/tarsio/references/api-reference.md +246 -0
  5. qqmusic_api_python-0.5.0/.agents/skills/uv-package-manager/SKILL.md +836 -0
  6. qqmusic_api_python-0.5.0/.github/ISSUE_TEMPLATE/bug.yml +79 -0
  7. qqmusic_api_python-0.5.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  8. qqmusic_api_python-0.5.0/.github/ISSUE_TEMPLATE/feature.yml +34 -0
  9. qqmusic_api_python-0.5.0/.github/PULL_REQUEST_TEMPLATE.md +14 -0
  10. qqmusic_api_python-0.5.0/.github/renovate.json +38 -0
  11. qqmusic_api_python-0.5.0/.github/workflows/checking.yaml +14 -0
  12. qqmusic_api_python-0.5.0/.github/workflows/docs.yml +69 -0
  13. qqmusic_api_python-0.5.0/.github/workflows/release.yml +58 -0
  14. qqmusic_api_python-0.5.0/.github/workflows/testing.yml +22 -0
  15. {qqmusic_api_python-0.4.1 → qqmusic_api_python-0.5.0}/.gitignore +6 -0
  16. qqmusic_api_python-0.5.0/.markdownlint-cli2.yaml +33 -0
  17. qqmusic_api_python-0.5.0/AGENTS.md +54 -0
  18. qqmusic_api_python-0.5.0/LICENSE +674 -0
  19. {qqmusic_api_python-0.4.1 → qqmusic_api_python-0.5.0}/PKG-INFO +41 -56
  20. qqmusic_api_python-0.5.0/README.md +56 -0
  21. qqmusic_api_python-0.5.0/assets/qq-music.svg +1 -0
  22. qqmusic_api_python-0.5.0/cliff.toml +94 -0
  23. qqmusic_api_python-0.5.0/docs/coding.md +237 -0
  24. qqmusic_api_python-0.5.0/docs/contributing.md +99 -0
  25. qqmusic_api_python-0.5.0/docs/index.md +83 -0
  26. qqmusic_api_python-0.5.0/docs/reference/core/client.md +3 -0
  27. qqmusic_api_python-0.5.0/docs/reference/core/exception.md +21 -0
  28. qqmusic_api_python-0.5.0/docs/reference/core/request.md +3 -0
  29. qqmusic_api_python-0.5.0/docs/reference/core/versioning.md +3 -0
  30. qqmusic_api_python-0.5.0/docs/reference/model/album.md +3 -0
  31. qqmusic_api_python-0.5.0/docs/reference/model/base.md +3 -0
  32. qqmusic_api_python-0.5.0/docs/reference/model/comment.md +3 -0
  33. qqmusic_api_python-0.5.0/docs/reference/model/login.md +3 -0
  34. qqmusic_api_python-0.5.0/docs/reference/model/lyric.md +3 -0
  35. qqmusic_api_python-0.5.0/docs/reference/model/mv.md +3 -0
  36. qqmusic_api_python-0.5.0/docs/reference/model/recommend.md +3 -0
  37. qqmusic_api_python-0.5.0/docs/reference/model/request.md +3 -0
  38. qqmusic_api_python-0.5.0/docs/reference/model/search.md +3 -0
  39. qqmusic_api_python-0.5.0/docs/reference/model/singer.md +3 -0
  40. qqmusic_api_python-0.5.0/docs/reference/model/song.md +3 -0
  41. qqmusic_api_python-0.5.0/docs/reference/model/songlist.md +3 -0
  42. qqmusic_api_python-0.5.0/docs/reference/model/top.md +3 -0
  43. qqmusic_api_python-0.5.0/docs/reference/model/user.md +3 -0
  44. qqmusic_api_python-0.5.0/docs/reference/modules/album.md +3 -0
  45. qqmusic_api_python-0.5.0/docs/reference/modules/comment.md +3 -0
  46. qqmusic_api_python-0.5.0/docs/reference/modules/login.md +3 -0
  47. qqmusic_api_python-0.5.0/docs/reference/modules/lyric.md +3 -0
  48. qqmusic_api_python-0.5.0/docs/reference/modules/mv.md +3 -0
  49. qqmusic_api_python-0.5.0/docs/reference/modules/recommend.md +3 -0
  50. qqmusic_api_python-0.5.0/docs/reference/modules/search.md +3 -0
  51. qqmusic_api_python-0.5.0/docs/reference/modules/singer.md +3 -0
  52. qqmusic_api_python-0.5.0/docs/reference/modules/song.md +3 -0
  53. qqmusic_api_python-0.5.0/docs/reference/modules/songlist.md +3 -0
  54. qqmusic_api_python-0.5.0/docs/reference/modules/top.md +3 -0
  55. qqmusic_api_python-0.5.0/docs/reference/modules/user.md +3 -0
  56. qqmusic_api_python-0.5.0/docs/release-notes.md +606 -0
  57. qqmusic_api_python-0.5.0/docs/tutorial/client.md +68 -0
  58. qqmusic_api_python-0.5.0/docs/tutorial/credential.md +83 -0
  59. qqmusic_api_python-0.5.0/docs/tutorial/start.md +69 -0
  60. qqmusic_api_python-0.5.0/examples/download_song.py +30 -0
  61. qqmusic_api_python-0.5.0/examples/phone_login.py +41 -0
  62. qqmusic_api_python-0.5.0/examples/qrcode_login.py +74 -0
  63. qqmusic_api_python-0.5.0/prek.toml +74 -0
  64. {qqmusic_api_python-0.4.1 → qqmusic_api_python-0.5.0}/pyproject.toml +92 -65
  65. qqmusic_api_python-0.5.0/qqmusic_api/__init__.py +32 -0
  66. qqmusic_api_python-0.5.0/qqmusic_api/algorithms/__init__.py +48 -0
  67. qqmusic_api_python-0.5.0/qqmusic_api/algorithms/sign.py +58 -0
  68. {qqmusic_api_python-0.4.1/qqmusic_api/utils → qqmusic_api_python-0.5.0/qqmusic_api/algorithms}/tripledes.py +18 -19
  69. qqmusic_api_python-0.5.0/qqmusic_api/core/__init__.py +47 -0
  70. qqmusic_api_python-0.5.0/qqmusic_api/core/client.py +749 -0
  71. qqmusic_api_python-0.5.0/qqmusic_api/core/exceptions.py +289 -0
  72. qqmusic_api_python-0.5.0/qqmusic_api/core/request.py +357 -0
  73. qqmusic_api_python-0.5.0/qqmusic_api/core/versioning.py +238 -0
  74. qqmusic_api_python-0.5.0/qqmusic_api/models/__init__.py +7 -0
  75. qqmusic_api_python-0.5.0/qqmusic_api/models/album.py +72 -0
  76. qqmusic_api_python-0.5.0/qqmusic_api/models/base.py +307 -0
  77. qqmusic_api_python-0.5.0/qqmusic_api/models/comment.py +205 -0
  78. qqmusic_api_python-0.5.0/qqmusic_api/models/login.py +116 -0
  79. qqmusic_api_python-0.5.0/qqmusic_api/models/lyric.py +41 -0
  80. qqmusic_api_python-0.5.0/qqmusic_api/models/mv.py +118 -0
  81. qqmusic_api_python-0.5.0/qqmusic_api/models/recommend.py +174 -0
  82. qqmusic_api_python-0.5.0/qqmusic_api/models/request.py +179 -0
  83. qqmusic_api_python-0.5.0/qqmusic_api/models/search.py +276 -0
  84. qqmusic_api_python-0.5.0/qqmusic_api/models/singer.py +471 -0
  85. qqmusic_api_python-0.5.0/qqmusic_api/models/song.py +389 -0
  86. qqmusic_api_python-0.5.0/qqmusic_api/models/songlist.py +74 -0
  87. qqmusic_api_python-0.5.0/qqmusic_api/models/top.py +118 -0
  88. qqmusic_api_python-0.5.0/qqmusic_api/models/user.py +418 -0
  89. qqmusic_api_python-0.5.0/qqmusic_api/modules/__init__.py +29 -0
  90. qqmusic_api_python-0.5.0/qqmusic_api/modules/_base.py +198 -0
  91. qqmusic_api_python-0.5.0/qqmusic_api/modules/album.py +53 -0
  92. qqmusic_api_python-0.5.0/qqmusic_api/modules/comment.py +154 -0
  93. qqmusic_api_python-0.5.0/qqmusic_api/modules/login.py +590 -0
  94. qqmusic_api_python-0.5.0/qqmusic_api/modules/login_utils.py +222 -0
  95. qqmusic_api_python-0.5.0/qqmusic_api/modules/lyric.py +50 -0
  96. qqmusic_api_python-0.5.0/qqmusic_api/modules/mv.py +70 -0
  97. qqmusic_api_python-0.5.0/qqmusic_api/modules/recommend.py +81 -0
  98. qqmusic_api_python-0.5.0/qqmusic_api/modules/search.py +144 -0
  99. qqmusic_api_python-0.5.0/qqmusic_api/modules/singer.py +290 -0
  100. qqmusic_api_python-0.5.0/qqmusic_api/modules/song.py +359 -0
  101. qqmusic_api_python-0.5.0/qqmusic_api/modules/songlist.py +169 -0
  102. qqmusic_api_python-0.5.0/qqmusic_api/modules/top.py +49 -0
  103. qqmusic_api_python-0.5.0/qqmusic_api/modules/user.py +306 -0
  104. qqmusic_api_python-0.5.0/qqmusic_api/utils/__init__.py +15 -0
  105. qqmusic_api_python-0.5.0/qqmusic_api/utils/common.py +110 -0
  106. qqmusic_api_python-0.5.0/qqmusic_api/utils/device.py +188 -0
  107. qqmusic_api_python-0.5.0/qqmusic_api/utils/mqtt.py +661 -0
  108. qqmusic_api_python-0.5.0/qqmusic_api/utils/qimei.py +250 -0
  109. qqmusic_api_python-0.5.0/qqmusic_api/utils/retry.py +116 -0
  110. qqmusic_api_python-0.5.0/scripts/ag-1.py +37 -0
  111. qqmusic_api_python-0.5.0/tests/conftest.py +96 -0
  112. qqmusic_api_python-0.5.0/tests/test_album.py +54 -0
  113. qqmusic_api_python-0.5.0/tests/test_comment.py +38 -0
  114. qqmusic_api_python-0.5.0/tests/test_login.py +97 -0
  115. qqmusic_api_python-0.5.0/tests/test_login_utils.py +71 -0
  116. qqmusic_api_python-0.5.0/tests/test_lyric.py +39 -0
  117. qqmusic_api_python-0.5.0/tests/test_mv.py +27 -0
  118. qqmusic_api_python-0.5.0/tests/test_recommend.py +37 -0
  119. qqmusic_api_python-0.5.0/tests/test_search.py +59 -0
  120. qqmusic_api_python-0.5.0/tests/test_singer.py +177 -0
  121. qqmusic_api_python-0.5.0/tests/test_song.py +106 -0
  122. qqmusic_api_python-0.5.0/tests/test_songlist.py +59 -0
  123. qqmusic_api_python-0.5.0/tests/test_top.py +55 -0
  124. qqmusic_api_python-0.5.0/tests/test_user.py +103 -0
  125. qqmusic_api_python-0.5.0/uv.lock +1487 -0
  126. qqmusic_api_python-0.5.0/zensical.toml +154 -0
  127. qqmusic_api_python-0.4.1/LICENSE +0 -21
  128. qqmusic_api_python-0.4.1/README.md +0 -82
  129. qqmusic_api_python-0.4.1/qqmusic_api/__init__.py +0 -36
  130. qqmusic_api_python-0.4.1/qqmusic_api/album.py +0 -58
  131. qqmusic_api_python-0.4.1/qqmusic_api/comment.py +0 -147
  132. qqmusic_api_python-0.4.1/qqmusic_api/exceptions/__init__.py +0 -17
  133. qqmusic_api_python-0.4.1/qqmusic_api/exceptions/api_exception.py +0 -67
  134. qqmusic_api_python-0.4.1/qqmusic_api/login.py +0 -556
  135. qqmusic_api_python-0.4.1/qqmusic_api/lyric.py +0 -68
  136. qqmusic_api_python-0.4.1/qqmusic_api/mv.py +0 -79
  137. qqmusic_api_python-0.4.1/qqmusic_api/recommend.py +0 -52
  138. qqmusic_api_python-0.4.1/qqmusic_api/search.py +0 -138
  139. qqmusic_api_python-0.4.1/qqmusic_api/singer.py +0 -411
  140. qqmusic_api_python-0.4.1/qqmusic_api/song.py +0 -319
  141. qqmusic_api_python-0.4.1/qqmusic_api/songlist.py +0 -146
  142. qqmusic_api_python-0.4.1/qqmusic_api/top.py +0 -34
  143. qqmusic_api_python-0.4.1/qqmusic_api/user.py +0 -206
  144. qqmusic_api_python-0.4.1/qqmusic_api/utils/__init__.py +0 -0
  145. qqmusic_api_python-0.4.1/qqmusic_api/utils/common.py +0 -96
  146. qqmusic_api_python-0.4.1/qqmusic_api/utils/credential.py +0 -137
  147. qqmusic_api_python-0.4.1/qqmusic_api/utils/device.py +0 -101
  148. qqmusic_api_python-0.4.1/qqmusic_api/utils/mqtt.py +0 -347
  149. qqmusic_api_python-0.4.1/qqmusic_api/utils/network.py +0 -458
  150. qqmusic_api_python-0.4.1/qqmusic_api/utils/qimei.py +0 -165
  151. qqmusic_api_python-0.4.1/qqmusic_api/utils/session.py +0 -99
  152. qqmusic_api_python-0.4.1/qqmusic_api/utils/sign.py +0 -36
  153. qqmusic_api_python-0.4.1/tests/test_album.py +0 -13
  154. qqmusic_api_python-0.4.1/tests/test_comment.py +0 -38
  155. qqmusic_api_python-0.4.1/tests/test_login.py +0 -42
  156. qqmusic_api_python-0.4.1/tests/test_lyric.py +0 -18
  157. qqmusic_api_python-0.4.1/tests/test_mv.py +0 -13
  158. qqmusic_api_python-0.4.1/tests/test_qimei.py +0 -5
  159. qqmusic_api_python-0.4.1/tests/test_recommend.py +0 -25
  160. qqmusic_api_python-0.4.1/tests/test_search.py +0 -33
  161. qqmusic_api_python-0.4.1/tests/test_session.py +0 -94
  162. qqmusic_api_python-0.4.1/tests/test_sign.py +0 -13
  163. qqmusic_api_python-0.4.1/tests/test_singer.py +0 -62
  164. qqmusic_api_python-0.4.1/tests/test_song.py +0 -55
  165. qqmusic_api_python-0.4.1/tests/test_songlist.py +0 -18
  166. qqmusic_api_python-0.4.1/tests/test_top.py +0 -13
  167. qqmusic_api_python-0.4.1/tests/test_user.py +0 -86
  168. qqmusic_api_python-0.4.1/web/README.md +0 -46
@@ -0,0 +1,1439 @@
1
+ ---
2
+ name: pydantic
3
+ description: Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation in FastAPI, Django, and configuration management.
4
+ progressive_disclosure:
5
+ entry_point:
6
+ - summary
7
+ - when_to_use
8
+ - quick_start
9
+ full_content: all
10
+ token_estimates:
11
+ entry_point: 70
12
+ full: 5500
13
+ ---
14
+
15
+ # Pydantic Validation Skill
16
+
17
+ ## Summary
18
+
19
+ Python data validation using type hints and runtime type checking with Pydantic v2's Rust-powered core for high-performance validation.
20
+
21
+ ## When to Use
22
+
23
+ * API request/response validation (FastAPI, Django)
24
+ * Settings and configuration management (env variables, config files)
25
+ * ORM model validation (SQLAlchemy integration)
26
+ * Data parsing and serialization (JSON, dict, custom formats)
27
+ * Type-safe data classes with automatic validation
28
+ * CLI argument parsing with type safety
29
+
30
+ ## Quick Start
31
+
32
+ ```python
33
+ from pydantic import BaseModel, Field, EmailStr
34
+ from datetime import datetime
35
+
36
+ class User(BaseModel):
37
+ id: int
38
+ name: str = Field(..., min_length=1, max_length=100)
39
+ email: EmailStr
40
+ created_at: datetime = Field(default_factory=datetime.now)
41
+ is_active: bool = True
42
+
43
+ # Validate data
44
+ user = User(id=1, name="Alice", email="alice@example.com")
45
+ print(user.model_dump()) # {'id': 1, 'name': 'Alice', ...}
46
+
47
+ # Automatic type coercion
48
+ user2 = User(id="2", name="Bob", email="bob@example.com")
49
+ assert user2.id == 2 # String "2" coerced to int
50
+
51
+ # Validation error
52
+ try:
53
+ User(id=3, name="", email="invalid")
54
+ except ValidationError as e:
55
+ print(e.errors())
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Core Concepts
61
+
62
+ ### BaseModel Foundation
63
+
64
+ ```python
65
+ from pydantic import BaseModel, ConfigDict
66
+
67
+ class Product(BaseModel):
68
+ model_config = ConfigDict(
69
+ str_strip_whitespace=True,
70
+ validate_assignment=True,
71
+ use_enum_values=True,
72
+ arbitrary_types_allowed=False
73
+ )
74
+
75
+ name: str
76
+ price: float
77
+ quantity: int = 0
78
+
79
+ # Usage
80
+ product = Product(name=" Widget ", price=19.99)
81
+ assert product.name == "Widget" # Whitespace stripped
82
+
83
+ # Validate on assignment
84
+ product.price = "29.99" # Auto-converts to float
85
+ ```
86
+
87
+ ### Field Configuration
88
+
89
+ ```python
90
+ from pydantic import Field, field_validator
91
+ from typing import Annotated
92
+
93
+ class Item(BaseModel):
94
+ # Field constraints
95
+ sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
96
+ price: float = Field(gt=0, le=10000)
97
+ stock: int = Field(ge=0, default=0)
98
+
99
+ # Annotated types (Pydantic v2)
100
+ quantity: Annotated[int, Field(ge=1, le=100)]
101
+
102
+ # Descriptions and examples
103
+ description: str = Field(
104
+ ...,
105
+ description="Product description",
106
+ examples=["High-quality widget"]
107
+ )
108
+
109
+ # Deprecated fields
110
+ old_field: str | None = Field(None, deprecated=True)
111
+
112
+ @field_validator('sku')
113
+ @classmethod
114
+ def validate_sku(cls, v: str) -> str:
115
+ if not v.startswith('ABC'):
116
+ raise ValueError('SKU must start with ABC')
117
+ return v
118
+ ```
119
+
120
+ ## Pydantic v2 Improvements
121
+
122
+ ### Migration from v1
123
+
124
+ ```python
125
+ # Pydantic v1
126
+ class OldModel(BaseModel):
127
+ class Config:
128
+ validate_assignment = True
129
+ json_encoders = {datetime: lambda v: v.isoformat()}
130
+
131
+ # Pydantic v2
132
+ class NewModel(BaseModel):
133
+ model_config = ConfigDict(
134
+ validate_assignment=True,
135
+ # json_encoders replaced by serializers
136
+ )
137
+
138
+ @model_serializer
139
+ def ser_model(self) -> dict:
140
+ return {...}
141
+
142
+ # Key changes:
143
+ # - .dict() → .model_dump()
144
+ # - .json() → .model_dump_json()
145
+ # - .parse_obj() → .model_validate()
146
+ # - .parse_raw() → .model_validate_json()
147
+ # - @validator → @field_validator
148
+ # - @root_validator → @model_validator
149
+ ```
150
+
151
+ ### Performance Improvements
152
+
153
+ ```python
154
+ # v2 uses Rust core (pydantic-core) for 5-50x speedup
155
+ from pydantic import BaseModel
156
+ import time
157
+
158
+ class Data(BaseModel):
159
+ values: list[int]
160
+ names: list[str]
161
+
162
+ # Benchmark
163
+ data = {'values': list(range(10000)), 'names': ['item'] * 10000}
164
+ start = time.perf_counter()
165
+ for _ in range(1000):
166
+ Data.model_validate(data)
167
+ elapsed = time.perf_counter() - start
168
+ print(f"Validated 1000 iterations in {elapsed:.2f}s")
169
+ ```
170
+
171
+ ## Field Types
172
+
173
+ ### Built-in Types
174
+
175
+ ```python
176
+ from pydantic import (
177
+ BaseModel, EmailStr, HttpUrl, UUID4,
178
+ FilePath, DirectoryPath, Json, SecretStr,
179
+ PositiveInt, NegativeFloat, conint, constr
180
+ )
181
+ from typing import Literal
182
+ from pathlib import Path
183
+
184
+ class Example(BaseModel):
185
+ # Email validation
186
+ email: EmailStr
187
+
188
+ # URL validation
189
+ website: HttpUrl
190
+
191
+ # UUID
192
+ id: UUID4
193
+
194
+ # File system paths
195
+ config_file: FilePath
196
+ data_dir: DirectoryPath
197
+
198
+ # JSON string → parsed object
199
+ metadata: Json[dict[str, str]]
200
+
201
+ # Secret (won't print in logs)
202
+ api_key: SecretStr
203
+
204
+ # Constrained types
205
+ age: PositiveInt
206
+ balance: NegativeFloat
207
+ username: constr(min_length=3, max_length=20, pattern=r'^[a-z]+$')
208
+ code: conint(ge=1000, le=9999)
209
+
210
+ # Literal types
211
+ status: Literal['pending', 'approved', 'rejected']
212
+ ```
213
+
214
+ ### Custom Types
215
+
216
+ ```python
217
+ from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
218
+ from pydantic_core import core_schema
219
+ from typing import Any
220
+
221
+ class Color:
222
+ def __init__(self, r: int, g: int, b: int):
223
+ self.r, self.g, self.b = r, g, b
224
+
225
+ @classmethod
226
+ def __get_pydantic_core_schema__(
227
+ cls, source_type: Any, handler: GetCoreSchemaHandler
228
+ ) -> core_schema.CoreSchema:
229
+ return core_schema.no_info_after_validator_function(
230
+ cls.validate,
231
+ core_schema.str_schema()
232
+ )
233
+
234
+ @classmethod
235
+ def validate(cls, v: str) -> 'Color':
236
+ if not v.startswith('#') or len(v) != 7:
237
+ raise ValueError('Invalid hex color')
238
+ r = int(v[1:3], 16)
239
+ g = int(v[3:5], 16)
240
+ b = int(v[5:7], 16)
241
+ return cls(r, g, b)
242
+
243
+ class Design(BaseModel):
244
+ primary_color: Color
245
+
246
+ # Usage
247
+ design = Design(primary_color='#FF5733')
248
+ assert design.primary_color.r == 255
249
+ ```
250
+
251
+ ## Validators
252
+
253
+ ### Field Validators
254
+
255
+ ```python
256
+ from pydantic import field_validator, model_validator
257
+
258
+ class Account(BaseModel):
259
+ username: str
260
+ password: str
261
+ password_confirm: str
262
+
263
+ @field_validator('username')
264
+ @classmethod
265
+ def username_alphanumeric(cls, v: str) -> str:
266
+ if not v.isalnum():
267
+ raise ValueError('must be alphanumeric')
268
+ return v
269
+
270
+ @field_validator('password')
271
+ @classmethod
272
+ def password_strong(cls, v: str) -> str:
273
+ if len(v) < 8:
274
+ raise ValueError('must be at least 8 characters')
275
+ if not any(c.isupper() for c in v):
276
+ raise ValueError('must contain uppercase letter')
277
+ return v
278
+
279
+ # Validate multiple fields
280
+ @field_validator('username', 'password')
281
+ @classmethod
282
+ def not_empty(cls, v: str) -> str:
283
+ if not v or not v.strip():
284
+ raise ValueError('must not be empty')
285
+ return v.strip()
286
+ ```
287
+
288
+ ### Model Validators
289
+
290
+ ```python
291
+ from pydantic import model_validator
292
+ from typing import Self
293
+
294
+ class DateRange(BaseModel):
295
+ start_date: datetime
296
+ end_date: datetime
297
+
298
+ @model_validator(mode='after')
299
+ def check_dates(self) -> Self:
300
+ if self.end_date < self.start_date:
301
+ raise ValueError('end_date must be after start_date')
302
+ return self
303
+
304
+ class Order(BaseModel):
305
+ items: list[str]
306
+ total: float
307
+ discount: float = 0
308
+
309
+ @model_validator(mode='before')
310
+ @classmethod
311
+ def calculate_total(cls, data: dict) -> dict:
312
+ # Pre-processing before validation
313
+ if isinstance(data, dict) and 'total' not in data:
314
+ data['total'] = len(data.get('items', [])) * 10.0
315
+ return data
316
+ ```
317
+
318
+ ### Root Validators (Wrap)
319
+
320
+ ```python
321
+ from pydantic import model_validator, ValidationInfo
322
+
323
+ class Config(BaseModel):
324
+ env: Literal['dev', 'prod']
325
+ debug: bool = False
326
+
327
+ @model_validator(mode='wrap')
328
+ @classmethod
329
+ def validate_config(cls, values: Any, handler, info: ValidationInfo):
330
+ # Call default validation
331
+ result = handler(values)
332
+
333
+ # Post-validation logic
334
+ if result.env == 'prod' and result.debug:
335
+ raise ValueError('debug cannot be True in production')
336
+
337
+ return result
338
+ ```
339
+
340
+ ## Type Coercion and Strict Mode
341
+
342
+ ```python
343
+ from pydantic import BaseModel, ConfigDict, ValidationError
344
+
345
+ # Coercive mode (default)
346
+ class CoerciveModel(BaseModel):
347
+ count: int
348
+ price: float
349
+
350
+ data = CoerciveModel(count="42", price="19.99")
351
+ assert data.count == 42 # String → int
352
+ assert data.price == 19.99 # String → float
353
+
354
+ # Strict mode
355
+ class StrictModel(BaseModel):
356
+ model_config = ConfigDict(strict=True)
357
+
358
+ count: int
359
+ price: float
360
+
361
+ try:
362
+ StrictModel(count="42", price="19.99") # Raises ValidationError
363
+ except ValidationError as e:
364
+ print("Strict mode: no coercion allowed")
365
+
366
+ # Per-field strict mode
367
+ class MixedModel(BaseModel):
368
+ flexible: int # Allows coercion
369
+ strict: Annotated[int, Field(strict=True)] # No coercion
370
+
371
+ MixedModel(flexible="1", strict=2) # OK
372
+ # MixedModel(flexible="1", strict="2") # ValidationError
373
+ ```
374
+
375
+ ## Nested Models and Recursive Types
376
+
377
+ ```python
378
+ from pydantic import BaseModel
379
+ from typing import ForwardRef
380
+
381
+ # Nested models
382
+ class Address(BaseModel):
383
+ street: str
384
+ city: str
385
+ country: str
386
+
387
+ class Company(BaseModel):
388
+ name: str
389
+ address: Address
390
+
391
+ company = Company(
392
+ name="ACME Corp",
393
+ address={'street': '123 Main St', 'city': 'NYC', 'country': 'USA'}
394
+ )
395
+
396
+ # Recursive types (tree structure)
397
+ class TreeNode(BaseModel):
398
+ value: int
399
+ children: list['TreeNode'] = []
400
+
401
+ TreeNode.model_rebuild() # Required for forward references
402
+
403
+ tree = TreeNode(
404
+ value=1,
405
+ children=[
406
+ TreeNode(value=2, children=[TreeNode(value=4)]),
407
+ TreeNode(value=3)
408
+ ]
409
+ )
410
+
411
+ # Self-referencing with ForwardRef
412
+ class Category(BaseModel):
413
+ name: str
414
+ parent: 'Category | None' = None
415
+ subcategories: list['Category'] = []
416
+
417
+ Category.model_rebuild()
418
+ ```
419
+
420
+ ## Generic Models
421
+
422
+ ```python
423
+ from pydantic import BaseModel
424
+ from typing import Generic, TypeVar
425
+
426
+ T = TypeVar('T')
427
+
428
+ class Response(BaseModel, Generic[T]):
429
+ success: bool
430
+ data: T
431
+ message: str = ''
432
+
433
+ class User(BaseModel):
434
+ id: int
435
+ name: str
436
+
437
+ # Usage with concrete type
438
+ user_response = Response[User](
439
+ success=True,
440
+ data=User(id=1, name='Alice')
441
+ )
442
+
443
+ # List response
444
+ list_response = Response[list[User]](
445
+ success=True,
446
+ data=[User(id=1, name='Alice'), User(id=2, name='Bob')]
447
+ )
448
+
449
+ # Generic repository pattern
450
+ class Repository(BaseModel, Generic[T]):
451
+ items: list[T]
452
+
453
+ def add(self, item: T) -> None:
454
+ self.items.append(item)
455
+
456
+ user_repo = Repository[User](items=[])
457
+ user_repo.add(User(id=1, name='Alice'))
458
+ ```
459
+
460
+ ## Serialization
461
+
462
+ ### Model Dump
463
+
464
+ ```python
465
+ from pydantic import BaseModel, Field, field_serializer
466
+
467
+ class Article(BaseModel):
468
+ title: str
469
+ content: str
470
+ tags: list[str]
471
+ metadata: dict[str, Any] = {}
472
+
473
+ # Serialization customization
474
+ @field_serializer('tags')
475
+ def serialize_tags(self, tags: list[str]) -> str:
476
+ return ','.join(tags)
477
+
478
+ article = Article(
479
+ title='Pydantic Guide',
480
+ content='...',
481
+ tags=['python', 'validation']
482
+ )
483
+
484
+ # Dump to dict
485
+ data = article.model_dump()
486
+ # {'title': 'Pydantic Guide', 'tags': 'python,validation', ...}
487
+
488
+ # Exclude fields
489
+ data = article.model_dump(exclude={'metadata'})
490
+
491
+ # Include only specific fields
492
+ data = article.model_dump(include={'title', 'tags'})
493
+
494
+ # Exclude unset fields
495
+ article2 = Article(title='Test', content='...', tags=[])
496
+ data = article2.model_dump(exclude_unset=True) # metadata excluded
497
+
498
+ # By alias
499
+ class AliasModel(BaseModel):
500
+ internal_name: str = Field(alias='externalName')
501
+
502
+ model = AliasModel(externalName='value')
503
+ model.model_dump(by_alias=True) # {'externalName': 'value'}
504
+ ```
505
+
506
+ ### JSON Serialization
507
+
508
+ ```python
509
+ from datetime import datetime
510
+ from pydantic import BaseModel, field_serializer
511
+
512
+ class Event(BaseModel):
513
+ name: str
514
+ timestamp: datetime
515
+
516
+ @field_serializer('timestamp')
517
+ def serialize_dt(self, dt: datetime) -> str:
518
+ return dt.isoformat()
519
+
520
+ event = Event(name='Deploy', timestamp=datetime.now())
521
+
522
+ # Dump to JSON string
523
+ json_str = event.model_dump_json()
524
+ # '{"name":"Deploy","timestamp":"2025-11-30T..."}'
525
+
526
+ # Pretty print
527
+ json_str = event.model_dump_json(indent=2)
528
+
529
+ # Parse from JSON
530
+ event2 = Event.model_validate_json(json_str)
531
+ ```
532
+
533
+ ### Custom Serializers
534
+
535
+ ```python
536
+ from pydantic import model_serializer
537
+
538
+ class User(BaseModel):
539
+ id: int
540
+ username: str
541
+ password: SecretStr
542
+
543
+ @model_serializer
544
+ def ser_model(self) -> dict[str, Any]:
545
+ return {
546
+ 'id': self.id,
547
+ 'username': self.username,
548
+ # Never serialize password
549
+ }
550
+
551
+ user = User(id=1, username='alice', password='secret123')
552
+ assert 'password' not in user.model_dump()
553
+ ```
554
+
555
+ ## Settings Management
556
+
557
+ ### BaseSettings
558
+
559
+ ```python
560
+ from pydantic_settings import BaseSettings, SettingsConfigDict
561
+ from pydantic import Field
562
+
563
+ class AppSettings(BaseSettings):
564
+ model_config = SettingsConfigDict(
565
+ env_file='.env',
566
+ env_file_encoding='utf-8',
567
+ env_prefix='APP_',
568
+ case_sensitive=False
569
+ )
570
+
571
+ # Environment variables
572
+ database_url: str
573
+ redis_url: str = 'redis://localhost:6379'
574
+ secret_key: SecretStr
575
+ debug: bool = False
576
+
577
+ # Nested settings
578
+ class SMTPSettings(BaseModel):
579
+ host: str
580
+ port: int = 587
581
+ username: str
582
+ password: SecretStr
583
+
584
+ smtp: SMTPSettings
585
+
586
+ # Reads from environment variables:
587
+ # APP_DATABASE_URL, APP_REDIS_URL, APP_SECRET_KEY, APP_DEBUG
588
+ # APP_SMTP__HOST, APP_SMTP__PORT, etc.
589
+
590
+ settings = AppSettings()
591
+ ```
592
+
593
+ ### Multi-Environment Settings
594
+
595
+ ```python
596
+ from functools import lru_cache
597
+
598
+ class Settings(BaseSettings):
599
+ environment: Literal['dev', 'staging', 'prod'] = 'dev'
600
+ database_url: str
601
+ api_key: SecretStr
602
+
603
+ model_config = SettingsConfigDict(
604
+ env_file='.env',
605
+ extra='ignore'
606
+ )
607
+
608
+ @property
609
+ def is_production(self) -> bool:
610
+ return self.environment == 'prod'
611
+
612
+ @lru_cache
613
+ def get_settings() -> Settings:
614
+ return Settings()
615
+
616
+ # Usage in FastAPI
617
+ from fastapi import Depends
618
+
619
+ @app.get('/config')
620
+ def get_config(settings: Settings = Depends(get_settings)):
621
+ return {'env': settings.environment}
622
+ ```
623
+
624
+ ## FastAPI Integration
625
+
626
+ ### Request/Response Models
627
+
628
+ ```python
629
+ from fastapi import FastAPI, HTTPException
630
+ from pydantic import BaseModel, EmailStr
631
+
632
+ app = FastAPI()
633
+
634
+ class UserCreate(BaseModel):
635
+ username: str = Field(min_length=3, max_length=50)
636
+ email: EmailStr
637
+ password: str = Field(min_length=8)
638
+
639
+ class UserResponse(BaseModel):
640
+ id: int
641
+ username: str
642
+ email: EmailStr
643
+
644
+ model_config = ConfigDict(from_attributes=True)
645
+
646
+ @app.post('/users', response_model=UserResponse)
647
+ def create_user(user: UserCreate):
648
+ # FastAPI auto-validates request body
649
+ # Returns only fields in UserResponse (password excluded)
650
+ return UserResponse(
651
+ id=1,
652
+ username=user.username,
653
+ email=user.email
654
+ )
655
+ ```
656
+
657
+ ### Query Parameters
658
+
659
+ ```python
660
+ from pydantic import BaseModel, Field
661
+ from fastapi import Query
662
+
663
+ class PaginationParams(BaseModel):
664
+ skip: int = Field(0, ge=0)
665
+ limit: int = Field(10, ge=1, le=100)
666
+
667
+ class SearchParams(BaseModel):
668
+ q: str = Field(..., min_length=1)
669
+ category: str | None = None
670
+ sort_by: Literal['date', 'relevance'] = 'relevance'
671
+
672
+ @app.get('/search')
673
+ def search(params: SearchParams = Query()):
674
+ return {'query': params.q, 'sort': params.sort_by}
675
+ ```
676
+
677
+ ### Response Model Customization
678
+
679
+ ```python
680
+ class DetailedUser(BaseModel):
681
+ id: int
682
+ username: str
683
+ email: EmailStr
684
+ created_at: datetime
685
+ last_login: datetime | None
686
+
687
+ @app.get('/users/{user_id}', response_model=DetailedUser)
688
+ def get_user(user_id: int, include_dates: bool = False):
689
+ user = DetailedUser(
690
+ id=user_id,
691
+ username='alice',
692
+ email='alice@example.com',
693
+ created_at=datetime.now(),
694
+ last_login=None
695
+ )
696
+
697
+ if not include_dates:
698
+ return user.model_dump(exclude={'created_at', 'last_login'})
699
+ return user
700
+ ```
701
+
702
+ ## SQLAlchemy Integration
703
+
704
+ ### ORM Models with Pydantic
705
+
706
+ ```python
707
+ from sqlalchemy import Column, Integer, String, DateTime
708
+ from sqlalchemy.orm import DeclarativeBase
709
+ from pydantic import BaseModel, ConfigDict
710
+
711
+ class Base(DeclarativeBase):
712
+ pass
713
+
714
+ # SQLAlchemy ORM model
715
+ class UserDB(Base):
716
+ __tablename__ = 'users'
717
+
718
+ id = Column(Integer, primary_key=True)
719
+ username = Column(String(50), unique=True)
720
+ email = Column(String(100))
721
+ created_at = Column(DateTime, default=datetime.utcnow)
722
+
723
+ # Pydantic model for validation
724
+ class UserSchema(BaseModel):
725
+ model_config = ConfigDict(from_attributes=True)
726
+
727
+ id: int
728
+ username: str
729
+ email: EmailStr
730
+ created_at: datetime
731
+
732
+ # Usage
733
+ from sqlalchemy.orm import Session
734
+
735
+ def get_user(db: Session, user_id: int) -> UserSchema:
736
+ user = db.query(UserDB).filter(UserDB.id == user_id).first()
737
+ return UserSchema.model_validate(user) # ORM → Pydantic
738
+ ```
739
+
740
+ ### Hybrid Approach
741
+
742
+ ```python
743
+ from pydantic import BaseModel
744
+
745
+ class UserBase(BaseModel):
746
+ username: str
747
+ email: EmailStr
748
+
749
+ class UserCreate(UserBase):
750
+ password: str
751
+
752
+ class UserUpdate(BaseModel):
753
+ username: str | None = None
754
+ email: EmailStr | None = None
755
+ password: str | None = None
756
+
757
+ class UserInDB(UserBase):
758
+ model_config = ConfigDict(from_attributes=True)
759
+
760
+ id: int
761
+ created_at: datetime
762
+ password_hash: str
763
+
764
+ # CRUD operations
765
+ def create_user(db: Session, user: UserCreate) -> UserInDB:
766
+ db_user = UserDB(
767
+ username=user.username,
768
+ email=user.email,
769
+ password_hash=hash_password(user.password)
770
+ )
771
+ db.add(db_user)
772
+ db.commit()
773
+ db.refresh(db_user)
774
+ return UserInDB.model_validate(db_user)
775
+ ```
776
+
777
+ ## Django Integration
778
+
779
+ ### Django Model Validation
780
+
781
+ ```python
782
+ from django.db import models
783
+ from pydantic import BaseModel, field_validator
784
+
785
+ # Django model
786
+ class Article(models.Model):
787
+ title = models.CharField(max_length=200)
788
+ content = models.TextField()
789
+ published = models.BooleanField(default=False)
790
+
791
+ # Pydantic schema
792
+ class ArticleSchema(BaseModel):
793
+ model_config = ConfigDict(from_attributes=True)
794
+
795
+ title: str = Field(max_length=200)
796
+ content: str
797
+ published: bool = False
798
+
799
+ @field_validator('content')
800
+ @classmethod
801
+ def validate_content(cls, v: str) -> str:
802
+ if len(v) < 100:
803
+ raise ValueError('Content too short')
804
+ return v
805
+
806
+ # Usage in Django views
807
+ from django.http import JsonResponse
808
+ from django.views.decorators.http import require_http_methods
809
+
810
+ @require_http_methods(['POST'])
811
+ def create_article(request):
812
+ try:
813
+ data = ArticleSchema.model_validate_json(request.body)
814
+ article = Article.objects.create(**data.model_dump())
815
+ return JsonResponse({'id': article.id})
816
+ except ValidationError as e:
817
+ return JsonResponse({'errors': e.errors()}, status=400)
818
+ ```
819
+
820
+ ## Computed Fields
821
+
822
+ ```python
823
+ from pydantic import computed_field
824
+
825
+ class Rectangle(BaseModel):
826
+ width: float
827
+ height: float
828
+
829
+ @computed_field
830
+ @property
831
+ def area(self) -> float:
832
+ return self.width * self.height
833
+
834
+ @computed_field
835
+ @property
836
+ def perimeter(self) -> float:
837
+ return 2 * (self.width + self.height)
838
+
839
+ rect = Rectangle(width=10, height=5)
840
+ assert rect.area == 50
841
+ assert rect.perimeter == 30
842
+
843
+ # Computed fields in serialization
844
+ data = rect.model_dump()
845
+ # {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}
846
+ ```
847
+
848
+ ## Custom Errors
849
+
850
+ ```python
851
+ from pydantic import BaseModel, field_validator, ValidationError
852
+ from pydantic_core import PydanticCustomError
853
+
854
+ class StrictUser(BaseModel):
855
+ username: str
856
+ age: int
857
+
858
+ @field_validator('username')
859
+ @classmethod
860
+ def validate_username(cls, v: str) -> str:
861
+ if len(v) < 3:
862
+ raise PydanticCustomError(
863
+ 'username_too_short',
864
+ 'Username must be at least 3 characters',
865
+ {'min_length': 3, 'actual_length': len(v)}
866
+ )
867
+ return v
868
+
869
+ @field_validator('age')
870
+ @classmethod
871
+ def validate_age(cls, v: int) -> int:
872
+ if v < 18:
873
+ raise PydanticCustomError(
874
+ 'underage',
875
+ 'User must be at least 18 years old',
876
+ {'age': v, 'minimum_age': 18}
877
+ )
878
+ return v
879
+
880
+ # Custom error handling
881
+ try:
882
+ StrictUser(username='ab', age=16)
883
+ except ValidationError as e:
884
+ for error in e.errors():
885
+ print(f"{error['type']}: {error['msg']}")
886
+ print(f"Context: {error.get('ctx')}")
887
+ ```
888
+
889
+ ## Performance Optimization
890
+
891
+ ### V2 Rust Core Benefits
892
+
893
+ ```python
894
+ # Pydantic v2 uses pydantic-core (Rust) for:
895
+ # - 5-50x faster validation
896
+ # - Lower memory usage
897
+ # - Better error messages
898
+ # - Improved JSON parsing
899
+
900
+ import timeit
901
+ from pydantic import BaseModel
902
+
903
+ class Data(BaseModel):
904
+ values: list[int]
905
+ names: list[str]
906
+ metadata: dict[str, Any]
907
+
908
+ # Benchmark
909
+ data_dict = {
910
+ 'values': list(range(1000)),
911
+ 'names': ['item'] * 1000,
912
+ 'metadata': {'key': 'value'}
913
+ }
914
+
915
+ def validate():
916
+ Data.model_validate(data_dict)
917
+
918
+ time_taken = timeit.timeit(validate, number=10000)
919
+ print(f"10000 validations: {time_taken:.2f}s")
920
+ ```
921
+
922
+ ### Optimization Techniques
923
+
924
+ ```python
925
+ from pydantic import BaseModel, ConfigDict
926
+
927
+ class OptimizedModel(BaseModel):
928
+ model_config = ConfigDict(
929
+ # Validate assignment only when needed
930
+ validate_assignment=False,
931
+
932
+ # Disable validation for internal use
933
+ validate_default=False,
934
+
935
+ # Use slots for memory efficiency
936
+ # (Not available in Pydantic v2 BaseModel directly)
937
+ )
938
+
939
+ data: list[int]
940
+
941
+ # Reuse validators
942
+ from functools import lru_cache
943
+
944
+ @lru_cache(maxsize=128)
945
+ def get_validator(model_class):
946
+ return model_class.model_validate
947
+
948
+ # Bulk validation
949
+ def validate_bulk(items: list[dict]) -> list[Data]:
950
+ validator = get_validator(Data)
951
+ return [validator(item) for item in items]
952
+ ```
953
+
954
+ ## JSON Schema Generation
955
+
956
+ ```python
957
+ from pydantic import BaseModel, Field
958
+
959
+ class Product(BaseModel):
960
+ """Product model for catalog"""
961
+
962
+ id: int = Field(description="Unique product identifier")
963
+ name: str = Field(description="Product name", examples=["Widget"])
964
+ price: float = Field(gt=0, description="Price in USD")
965
+ tags: list[str] = Field(default=[], description="Product tags")
966
+
967
+ # Generate JSON Schema
968
+ schema = Product.model_json_schema()
969
+ print(json.dumps(schema, indent=2))
970
+ # {
971
+ # "title": "Product",
972
+ # "description": "Product model for catalog",
973
+ # "type": "object",
974
+ # "properties": {
975
+ # "id": {"type": "integer", "description": "Unique product identifier"},
976
+ # "name": {"type": "string", "description": "Product name"},
977
+ # ...
978
+ # },
979
+ # "required": ["id", "name", "price"]
980
+ # }
981
+
982
+ # OpenAPI compatible
983
+ from fastapi import FastAPI
984
+
985
+ app = FastAPI()
986
+
987
+ @app.post('/products')
988
+ def create_product(product: Product):
989
+ return product
990
+
991
+ # FastAPI auto-generates OpenAPI schema from Pydantic models
992
+ ```
993
+
994
+ ## Dataclass Integration
995
+
996
+ ```python
997
+ from pydantic.dataclasses import dataclass
998
+ from pydantic import Field
999
+
1000
+ @dataclass
1001
+ class User:
1002
+ id: int
1003
+ name: str = Field(min_length=1)
1004
+ email: str = Field(pattern=r'.+@.+\..+')
1005
+
1006
+ # Works like Pydantic BaseModel with validation
1007
+ user = User(id=1, name='Alice', email='alice@example.com')
1008
+
1009
+ # Validation on construction
1010
+ try:
1011
+ User(id=2, name='', email='invalid')
1012
+ except ValidationError as e:
1013
+ print(e.errors())
1014
+
1015
+ # Convert to Pydantic BaseModel
1016
+ from pydantic import BaseModel
1017
+
1018
+ class UserModel(BaseModel):
1019
+ model_config = ConfigDict(from_attributes=True)
1020
+
1021
+ id: int
1022
+ name: str
1023
+ email: str
1024
+
1025
+ user_model = UserModel.model_validate(user)
1026
+ ```
1027
+
1028
+ ## Testing Strategies
1029
+
1030
+ ### Unit Testing Models
1031
+
1032
+ ```python
1033
+ import pytest
1034
+ from pydantic import ValidationError
1035
+
1036
+ def test_user_validation():
1037
+ # Valid data
1038
+ user = User(id=1, name='Alice', email='alice@example.com')
1039
+ assert user.name == 'Alice'
1040
+
1041
+ # Invalid data
1042
+ with pytest.raises(ValidationError) as exc_info:
1043
+ User(id='invalid', name='Bob', email='bob@example.com')
1044
+
1045
+ errors = exc_info.value.errors()
1046
+ assert errors[0]['type'] == 'int_parsing'
1047
+
1048
+ def test_user_serialization():
1049
+ user = User(id=1, name='Alice', email='alice@example.com')
1050
+ data = user.model_dump()
1051
+
1052
+ assert data == {
1053
+ 'id': 1,
1054
+ 'name': 'Alice',
1055
+ 'email': 'alice@example.com'
1056
+ }
1057
+
1058
+ def test_nested_validation():
1059
+ company = Company(
1060
+ name='ACME',
1061
+ address={'street': '123 Main', 'city': 'NYC', 'country': 'USA'}
1062
+ )
1063
+ assert company.address.city == 'NYC'
1064
+ ```
1065
+
1066
+ ### Testing with Fixtures
1067
+
1068
+ ```python
1069
+ @pytest.fixture
1070
+ def sample_user_data():
1071
+ return {
1072
+ 'id': 1,
1073
+ 'name': 'Alice',
1074
+ 'email': 'alice@example.com'
1075
+ }
1076
+
1077
+ @pytest.fixture
1078
+ def sample_user(sample_user_data):
1079
+ return User(**sample_user_data)
1080
+
1081
+ def test_with_fixtures(sample_user):
1082
+ assert sample_user.name == 'Alice'
1083
+
1084
+ def test_invalid_email(sample_user_data):
1085
+ sample_user_data['email'] = 'invalid'
1086
+ with pytest.raises(ValidationError):
1087
+ User(**sample_user_data)
1088
+ ```
1089
+
1090
+ ### Property-Based Testing
1091
+
1092
+ ```python
1093
+ from hypothesis import given, strategies as st
1094
+
1095
+ @given(
1096
+ id=st.integers(min_value=1),
1097
+ name=st.text(min_size=1, max_size=100),
1098
+ email=st.emails()
1099
+ )
1100
+ def test_user_always_valid(id, name, email):
1101
+ user = User(id=id, name=name, email=email)
1102
+ assert user.id == id
1103
+ assert user.name == name
1104
+ assert user.email == email
1105
+ ```
1106
+
1107
+ ## Migration Guide (v1 → v2)
1108
+
1109
+ ### Key Changes
1110
+
1111
+ ```python
1112
+ # v1
1113
+ from pydantic import BaseModel
1114
+
1115
+ class OldModel(BaseModel):
1116
+ class Config:
1117
+ validate_assignment = True
1118
+ arbitrary_types_allowed = True
1119
+
1120
+ # Validators
1121
+ @validator('field')
1122
+ def validate_field(cls, v):
1123
+ return v
1124
+
1125
+ @root_validator
1126
+ def validate_model(cls, values):
1127
+ return values
1128
+
1129
+ # Serialization
1130
+ data = model.dict()
1131
+ json_str = model.json()
1132
+
1133
+ # Parsing
1134
+ model = OldModel.parse_obj(data)
1135
+ model = OldModel.parse_raw(json_str)
1136
+
1137
+ # v2
1138
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
1139
+
1140
+ class NewModel(BaseModel):
1141
+ model_config = ConfigDict(
1142
+ validate_assignment=True,
1143
+ arbitrary_types_allowed=True
1144
+ )
1145
+
1146
+ # Field validators
1147
+ @field_validator('field')
1148
+ @classmethod
1149
+ def validate_field(cls, v):
1150
+ return v
1151
+
1152
+ # Model validators
1153
+ @model_validator(mode='after')
1154
+ def validate_model(self):
1155
+ return self
1156
+
1157
+ # Serialization
1158
+ data = model.model_dump()
1159
+ json_str = model.model_dump_json()
1160
+
1161
+ # Parsing
1162
+ model = NewModel.model_validate(data)
1163
+ model = NewModel.model_validate_json(json_str)
1164
+ ```
1165
+
1166
+ ### Migration Checklist
1167
+
1168
+ * [ ] Replace `class Config` with `model_config = ConfigDict()`
1169
+ * [ ] Update `.dict()` → `.model_dump()`
1170
+ * [ ] Update `.json()` → `.model_dump_json()`
1171
+ * [ ] Update `.parse_obj()` → `.model_validate()`
1172
+ * [ ] Update `.parse_raw()` → `.model_validate_json()`
1173
+ * [ ] Update `@validator` → `@field_validator` with `@classmethod`
1174
+ * [ ] Update `@root_validator` → `@model_validator(mode='after')`
1175
+ * [ ] Review `json_encoders` → use `@field_serializer`
1176
+ * [ ] Test strict mode behavior changes
1177
+ * [ ] Update custom types to use `__get_pydantic_core_schema__`
1178
+
1179
+ ## Best Practices
1180
+
1181
+ ### Model Organization
1182
+
1183
+ ```python
1184
+ # Separate schemas by use case
1185
+ class UserBase(BaseModel):
1186
+ """Shared fields"""
1187
+ username: str
1188
+ email: EmailStr
1189
+
1190
+ class UserCreate(UserBase):
1191
+ """API request for creating user"""
1192
+ password: str
1193
+
1194
+ class UserUpdate(BaseModel):
1195
+ """API request for updating user (all optional)"""
1196
+ username: str | None = None
1197
+ email: EmailStr | None = None
1198
+ password: str | None = None
1199
+
1200
+ class UserInDB(UserBase):
1201
+ """Database representation"""
1202
+ model_config = ConfigDict(from_attributes=True)
1203
+
1204
+ id: int
1205
+ password_hash: str
1206
+ created_at: datetime
1207
+
1208
+ class UserResponse(UserBase):
1209
+ """API response (excludes sensitive data)"""
1210
+ id: int
1211
+ created_at: datetime
1212
+ ```
1213
+
1214
+ ### Validation Best Practices
1215
+
1216
+ ```python
1217
+ # Use Field for constraints, not validators
1218
+ class Good(BaseModel):
1219
+ age: int = Field(ge=0, le=150)
1220
+ email: EmailStr
1221
+
1222
+ class Bad(BaseModel):
1223
+ age: int
1224
+ email: str
1225
+
1226
+ @field_validator('age')
1227
+ @classmethod
1228
+ def validate_age(cls, v):
1229
+ if v < 0 or v > 150:
1230
+ raise ValueError('invalid age')
1231
+ return v
1232
+
1233
+ # Prefer composition over inheritance
1234
+ class TimestampMixin(BaseModel):
1235
+ created_at: datetime = Field(default_factory=datetime.utcnow)
1236
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
1237
+
1238
+ class User(TimestampMixin):
1239
+ username: str
1240
+ email: EmailStr
1241
+ ```
1242
+
1243
+ ### Error Handling
1244
+
1245
+ ```python
1246
+ from pydantic import ValidationError
1247
+
1248
+ def safe_validate(data: dict) -> User | None:
1249
+ try:
1250
+ return User.model_validate(data)
1251
+ except ValidationError as e:
1252
+ # Log validation errors
1253
+ logger.error(f"Validation failed: {e.errors()}")
1254
+ return None
1255
+
1256
+ def validate_with_details(data: dict):
1257
+ try:
1258
+ return User.model_validate(data)
1259
+ except ValidationError as e:
1260
+ # Return user-friendly errors
1261
+ return {
1262
+ 'success': False,
1263
+ 'errors': [
1264
+ {
1265
+ 'field': '.'.join(str(loc) for loc in err['loc']),
1266
+ 'message': err['msg'],
1267
+ 'type': err['type']
1268
+ }
1269
+ for err in e.errors()
1270
+ ]
1271
+ }
1272
+ ```
1273
+
1274
+ ## Common Patterns
1275
+
1276
+ ### API Response Wrapper
1277
+
1278
+ ```python
1279
+ from typing import Generic, TypeVar
1280
+
1281
+ T = TypeVar('T')
1282
+
1283
+ class APIResponse(BaseModel, Generic[T]):
1284
+ success: bool
1285
+ data: T | None = None
1286
+ error: str | None = None
1287
+ metadata: dict[str, Any] = {}
1288
+
1289
+ # Usage
1290
+ user_response = APIResponse[User](
1291
+ success=True,
1292
+ data=User(id=1, name='Alice', email='alice@example.com')
1293
+ )
1294
+
1295
+ error_response = APIResponse[User](
1296
+ success=False,
1297
+ error='User not found'
1298
+ )
1299
+ ```
1300
+
1301
+ ### Pagination
1302
+
1303
+ ```python
1304
+ class PaginatedResponse(BaseModel, Generic[T]):
1305
+ items: list[T]
1306
+ total: int
1307
+ page: int
1308
+ page_size: int
1309
+
1310
+ @computed_field
1311
+ @property
1312
+ def total_pages(self) -> int:
1313
+ return (self.total + self.page_size - 1) // self.page_size
1314
+
1315
+ users = PaginatedResponse[User](
1316
+ items=[...],
1317
+ total=100,
1318
+ page=1,
1319
+ page_size=10
1320
+ )
1321
+ assert users.total_pages == 10
1322
+ ```
1323
+
1324
+ ### Audit Fields
1325
+
1326
+ ```python
1327
+ class AuditMixin(BaseModel):
1328
+ created_at: datetime = Field(default_factory=datetime.utcnow)
1329
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
1330
+ created_by: int | None = None
1331
+ updated_by: int | None = None
1332
+
1333
+ class Document(AuditMixin):
1334
+ title: str
1335
+ content: str
1336
+
1337
+ @model_validator(mode='before')
1338
+ @classmethod
1339
+ def update_timestamp(cls, data: dict) -> dict:
1340
+ if isinstance(data, dict):
1341
+ data['updated_at'] = datetime.utcnow()
1342
+ return data
1343
+ ```
1344
+
1345
+ ## Related Skills
1346
+
1347
+ When using Pydantic, consider these complementary skills:
1348
+
1349
+ * **fastapi-local-dev**: FastAPI development server patterns with Pydantic integration
1350
+ * **sqlalchemy**: SQLAlchemy ORM patterns for database models with Pydantic validation
1351
+ * **django**: Django framework integration with Pydantic schemas
1352
+ * **pytest**: Testing strategies for Pydantic models and validation
1353
+
1354
+ ### Quick FastAPI Integration Reference (Inlined for Standalone Use)
1355
+
1356
+ ```python
1357
+ # FastAPI with Pydantic (basic pattern)
1358
+ from fastapi import FastAPI, HTTPException
1359
+ from pydantic import BaseModel, EmailStr
1360
+
1361
+ app = FastAPI()
1362
+
1363
+ class UserCreate(BaseModel):
1364
+ username: str
1365
+ email: EmailStr
1366
+ password: str
1367
+
1368
+ class UserResponse(BaseModel):
1369
+ id: int
1370
+ username: str
1371
+ email: EmailStr
1372
+
1373
+ model_config = ConfigDict(from_attributes=True)
1374
+
1375
+ @app.post('/users', response_model=UserResponse)
1376
+ def create_user(user: UserCreate):
1377
+ # FastAPI auto-validates using Pydantic
1378
+ # response_model filters out password
1379
+ return UserResponse(id=1, username=user.username, email=user.email)
1380
+ ```
1381
+
1382
+ ### Quick SQLAlchemy Integration Reference (Inlined for Standalone Use)
1383
+
1384
+ ```python
1385
+ # SQLAlchemy 2.0 with Pydantic validation
1386
+ from sqlalchemy import Column, Integer, String
1387
+ from sqlalchemy.orm import DeclarativeBase
1388
+ from pydantic import BaseModel, ConfigDict
1389
+
1390
+ class Base(DeclarativeBase):
1391
+ pass
1392
+
1393
+ class UserDB(Base):
1394
+ __tablename__ = 'users'
1395
+ id = Column(Integer, primary_key=True)
1396
+ username = Column(String(50))
1397
+ email = Column(String(100))
1398
+
1399
+ class UserSchema(BaseModel):
1400
+ model_config = ConfigDict(from_attributes=True)
1401
+ id: int
1402
+ username: str
1403
+ email: str
1404
+
1405
+ # Convert ORM to Pydantic
1406
+ user_orm = db.query(UserDB).first()
1407
+ user_validated = UserSchema.model_validate(user_orm)
1408
+ ```
1409
+
1410
+ ### Quick Pytest Testing Reference (Inlined for Standalone Use)
1411
+
1412
+ ```python
1413
+ # Testing Pydantic models with pytest
1414
+ import pytest
1415
+ from pydantic import ValidationError
1416
+
1417
+ def test_user_validation():
1418
+ user = User(id=1, name='Alice', email='alice@example.com')
1419
+ assert user.name == 'Alice'
1420
+
1421
+ def test_validation_error():
1422
+ with pytest.raises(ValidationError) as exc_info:
1423
+ User(id='invalid', name='Bob', email='bob@example.com')
1424
+ errors = exc_info.value.errors()
1425
+ assert errors[0]['type'] == 'int_parsing'
1426
+
1427
+ @pytest.fixture
1428
+ def sample_user():
1429
+ return User(id=1, name='Alice', email='alice@example.com')
1430
+ ```
1431
+
1432
+ [Full integration patterns available in respective skills if deployed together]
1433
+
1434
+ ## Additional Resources
1435
+
1436
+ * [Pydantic Documentation](https://docs.pydantic.dev/)
1437
+ * [Migration Guide v1→v2](https://docs.pydantic.dev/latest/migration/)
1438
+ * [Performance Benchmarks](https://docs.pydantic.dev/latest/concepts/performance/)
1439
+ * [JSON Schema Integration](https://docs.pydantic.dev/latest/concepts/json_schema/)