🔝 الدليل الرئيسي للسلسلة: بايثون: لغة ومنظومة وأطر للمطوّرين
أصبح Pydantic v2 في 2026 المعيار للتحقق والتسلسل في Python الحديثة. الإصدار 2.13.4 الصادر في 6 مايو 2026، المُدفوع بـ pydantic-core المكتوب بـ Rust، يتحقق من الكائنات أسرع 5 إلى 10 مرات من v1 ويُغذّي المنظومة: FastAPI، SQLModel، BentoML، Pydantic Settings، إعداد خطوط أنابيب ML. للخروج من أرض « أتحقق يدويًا بـ if » والانتقال إلى نماذج تعريفية نظيفة، عدة أفكار تُهيكل الاستخدام: نماذج BaseModel، أنواع مُقيَّدة، validators، التسلسل حسب الوضع، settings من متغيّرات البيئة، وتوافق dataclass. يُسلسلها هذا الدرس خطوة بخطوة.
المتطلبات
- Python 3.13.13 أو 3.14.5 (راجع تثبيت Python 3)
- أساسيات type hints (
list[int]،dict[str, X]،Optional) - الوقت المُقدَّر: 75 دقيقة
الخطوة 1 — تثبيت Pydantic v2 وأول نموذج
Pydantic v2 يُثبَّت بأمر واحد ويعمل دون إعداد. النموذج الأساسي BaseModel يقبل تعليقات أنواع Python القياسية ويُقدّم مجانًا: التحقق، الـ coercion (str→int إن أمكن)، تسلسل JSON، وتوليد JSON Schema.
uv add pydantic
# ou
pip install --upgrade "pydantic>=2.13"
from pydantic import BaseModel, Field
from datetime import datetime
class Utilisateur(BaseModel):
id: int
email: str
nom: str = Field(min_length=2, max_length=100)
actif: bool = True
cree_le: datetime = Field(default_factory=datetime.now)
user = Utilisateur(id=42, email="alice@example.com", nom="Alice")
print(user.model_dump())
ثلاثة جوانب يجب فهمها. تعليقات الأنواع لم تعد زخرفية — Pydantic يستخدمها للتحقق فعلًا. Field(min_length=2, max_length=100) يُضيف قيودًا دون تغيير النوع الظاهر. default_factory يستدعي الدالة عند كل إنشاء (مقابل default=datetime.now() الذي يُجمّد القيمة لحظة الاستيراد). model_dump() يحل محل .dict() القديم من v1.
الخطوة 2 — أنواع مُقيَّدة وتحقق صارم
Pydantic v2 يُسلّم مكتبة غنية من الأنواع المُقيَّدة: EmailStr، HttpUrl، PositiveInt، conint، constr، Annotated مع Field.
from pydantic import BaseModel, EmailStr, HttpUrl, PositiveInt, Field
from typing import Annotated
Age = Annotated[int, Field(ge=0, le=150)]
Slug = Annotated[str, Field(pattern=r"^[a-z0-9-]+$", max_length=80)]
class Auteur(BaseModel):
email: EmailStr
site: HttpUrl
age: Age
slug: Slug
nb_articles: PositiveInt
biographie: Annotated[str, Field(max_length=500)] | None = None
class Config(BaseModel):
model_config = {"strict": True}
port: int # refuse "8080" (str), accepte 8080 (int)
الوضع الصارم ثمين لـ APIs الحساسة: يرفض التحويلات الضمنية التي قد تُخفي bugs. لمشروع عادي، الوضع المتساهل الافتراضي مناسب لـ payloads HTTP؛ الوضع الصارم لإعداد داخلي حيث الصرامة أهم من المرونة.
الخطوة 3 — Validators field وmodel
عندما لا تكفي القيود المدمجة، نكتب validator. Pydantic v2 يميّز بين @field_validator للتحقق من حقل بعد coercion نوعه، و@model_validator للتحقق من الكائن كاملًا (مفيد لقواعد متقاطعة بين الحقول).
from pydantic import BaseModel, field_validator, model_validator
from datetime import datetime, timezone
class Evenement(BaseModel):
debut: datetime
fin: datetime
capacite: int
@field_validator("debut", "fin", mode="after")
@classmethod
def doit_etre_aware(cls, v: datetime) -> datetime:
if v.tzinfo is None:
raise ValueError("Datetime doit être timezone-aware (UTC ou autre)")
return v.astimezone(timezone.utc)
@model_validator(mode="after")
def fin_apres_debut(self) -> "Evenement":
if self.fin <= self.debut:
raise ValueError("La fin doit être strictement après le début")
return self
الـ mode يُحدّد ترتيب التنفيذ. mode="before" يستقبل القيمة الخام قبل coercion. mode="after" يستقبل القيمة بعد coercion إلى النوع الهدف. model_validator mode="after" يستقبل المثيل الكامل لفحص الثوابت بين الحقول.
الخطوة 4 — التسلسل وmodel_dump
user = Utilisateur(id=42, email="alice@example.com", nom="Alice")
# Vers dict
data = user.model_dump()
# Vers JSON string
payload = user.model_dump_json(indent=2)
# Exclure des champs sensibles
public = user.model_dump(exclude={"email"})
# Inclure seulement certains champs
minimal = user.model_dump(include={"id", "nom"})
# Champs avec alias (pour APIs externes)
class UserDto(BaseModel):
id_externe: int = Field(alias="external_id")
nom_complet: str = Field(alias="full_name")
model_config = {"populate_by_name": True}
dto = UserDto.model_validate_json('{"external_id": 1, "full_name": "Bob"}')
print(dto.model_dump_json(by_alias=True))
الـ aliasing حاسم عند الترجمة بين schema snake_case داخلي وschema camelCase خارجي (نموذجي في APIs Java/Node). populate_by_name=True يقبل كلا الشكلين في الإدخال. تتجنّب هذه المرونة تلوّث الأسماء الداخلية بـ Python باصطلاحات أجنبية.
الخطوة 5 — Pydantic Settings: الإعداد من متغيّرات البيئة
uv add pydantic-settings
from pydantic import Field, HttpUrl, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Reglages(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="APP_",
case_sensitive=False,
extra="forbid"
)
nom_service: str = "api-itsc"
debug: bool = False
database_url: str
redis_url: str = "redis://localhost:6379/0"
secret_key: SecretStr # ne sera pas loggé en clair
workers: int = Field(default=4, ge=1, le=64)
api_base: HttpUrl
reglages = Reglages()
print(reglages.nom_service)
print(reglages.secret_key.get_secret_value())
ثلاثة عناصر هيكلية. SecretStr يُخفي القيمة في repr() والـ logs (يعرض '**********')، يمنع التسريبات العرضية. env_prefix="APP_" يُربط تلقائيًا APP_DATABASE_URL → database_url. extra="forbid" يرفض متغيّرات البيئة غير المُعلَنة. لخدمة Kubernetes، نُجمع ملف .env محلي للتطوير ومتغيّرات بيئة مُحقَنة في الإنتاج، بدون تغيير الكود.
الخطوة 6 — نماذج عامة ووراثة
Pydantic v2 يدعم النماذج العامة عبر TypeVar وGeneric. مفيد بشكل خاص لتوحيد ردود API التي تُغلّف بيانات بأنواع متغيّرة.
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class Reponse(BaseModel, Generic[T]):
succes: bool
donnees: T | None = None
erreur: str | None = None
request_id: str
class Article(BaseModel):
id: int
titre: str
reponse: Reponse[Article] = Reponse[Article](
succes=True,
donnees=Article(id=1, titre="Hello"),
request_id="req-abc"
)
class Pagination(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
par_page: int
المُتحقق من الأنواع (mypy، pyright) يفهم هذه العموميات ويتحقق ثابتًا من أن reponse.donnees من النوع Article | None. وقت التنفيذ، Pydantic يتحقق فعلًا من الهيكل المتداخل. الجمع بين الثابت والـ runtime يُعطي شبكة أمان متينة لا يوفّرها dicts.
الخطوة 7 — تكامل FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UtilisateurCreation(BaseModel):
email: EmailStr
nom: str
age: int = Field(ge=0, le=150)
class UtilisateurReponse(BaseModel):
id: int
email: EmailStr
nom: str
cree_le: datetime
@app.post("/users", response_model=UtilisateurReponse, status_code=201)
async def creer_user(payload: UtilisateurCreation) -> UtilisateurReponse:
user = await db.creer_utilisateur(**payload.model_dump())
return user
@app.get("/users/{user_id}", response_model=UtilisateurReponse)
async def lire_user(user_id: int) -> UtilisateurReponse:
user = await db.lire_utilisateur(user_id)
if user is None:
raise HTTPException(status_code=404, detail="Utilisateur introuvable")
return user
الـ response_model يُرشّح تلقائيًا الحقول الحساسة. password_hash في نموذج DB لن يظهر أبدًا في الرد إذا لم يكن في UtilisateurReponse. لاختبارات هذه الكدسة، راجع درس Pytest المتقدم.
الخطوة 8 — تسلسل مخصص وcomputed fields
from pydantic import BaseModel, field_serializer, computed_field
from datetime import datetime, timezone
class Article(BaseModel):
titre: str
contenu: str
publie_le: datetime
@field_serializer("publie_le")
def serialize_date(self, v: datetime, _info) -> str:
return v.astimezone(timezone.utc).isoformat()
@computed_field
@property
def nb_mots(self) -> int:
return len(self.contenu.split())
@computed_field
@property
def duree_lecture_min(self) -> int:
return max(1, self.nb_mots // 200)
الـ computed fields تظهر في model_dump() ووثائق OpenAPI كحقول عادية. يتجنّب ذلك حساب هذه المشتقات على جانب العميل أو تلوّث نموذج DB بأعمدة زائدة.
أخطاء شائعة
| العَرَض | السبب | الحل |
|---|---|---|
ValidationError غامض على حقل اختياري |
نوع Optional دون None افتراضي |
x: int | None = None |
ValidationError على ISO datetime |
صيغة غير قياسية | معالجة مسبقة بـ field_validator(mode="before") |
تحويل str إلى int صامتًا |
الوضع المتساهل افتراضيًا | تفعيل strict: True في model_config |
| حقل alias غير مُتعرَّف في POST | populate_by_name غائب |
إضافة populate_by_name: True |
| Secret مُسجَّل علنًا | str بدلًا من SecretStr |
SecretStr + .get_secret_value() |
| JSON Schema غير كامل | validators يدوية غير موثَّقة | Annotated[Field] بدل validators مخصصة إن أمكن |
الأسئلة الشائعة
Pydantic v1 أم v2؟
v2 لكل مشروع جديد. v1 يستقبل فقط تصحيحات أمنية منذ 2024. ترحيل v1→v2 موثّق بأداة bump-pydantic.
الأداء مقابل dataclasses؟
Pydantic v2 يتحقق عند كل إنشاء (تكلفة). dataclasses لا تتحقق أبدًا (مجاني لكن خطر). في 2026 مع pydantic-core بـ Rust، تكلفة Pydantic مهملة (~10 μs لنموذج بسيط).
SQLModel أم Pydantic + SQLAlchemy منفصلين؟
SQLModel لمشروع بسيط بتمثيل واحد. نماذج منفصلة (Pydantic لـ HTTP، SQLAlchemy لـ DB) للمشاريع المعقدة حيث schema API ≠ schema DB.
كيف نتحقق من النماذج المتداخلة؟
أصلي: auteurs: list[Auteur] في BaseModel يتحقق تكراريًا من كل auteur.
كيف نُولّد JSON Schema؟Utilisateur.model_json_schema() يُرجع JSON Schema كاملًا، متوافق مع draft 2020-12.