تطوير الويب

Pydantic v2 في 2026: التحقق والإعدادات وتكامل FastAPI

5 min de lecture

🔝 الدليل الرئيسي للسلسلة: بايثون: لغة ومنظومة وأطر للمطوّرين

أصبح 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_URLdatabase_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.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité