Python SQLAlchemy Pydantic en pratique : le code et les commandes qui comptent vraiment

Python SQLAlchemy Pydantic : l'essentiel en un article — vrai code, schémas et étapes concrètes, extraits d'un cours de 27 leçons.

Python SQLAlchemy Pydantic en pratique : le code et les commandes qui comptent vraiment

Pas de théorie interminable ici : on ouvre le terminal et on pratique. Voici l'essentiel de Python SQLAlchemy Pydantic, extrait directement d'un cours complet de 27 leçons — avec du vrai code que tu peux copier-coller maintenant.

tl;dr
  • Introduction
  • SQLAlchemy Core
  • ORM Declarative
  • Session et transactions
  • Requetes avancees
~$ cat ./parcours.md # Python SQLAlchemy Pydantic — 9 chapitres
01
Introduction
→ SQLAlchemy 2.0, la nouvelle ere→ Pydantic v2 et le typage strict
02
SQLAlchemy Core
→ Engine et connexion→ Tables et Metadata+ 1 autres leçons
03
ORM Déclarative
→ Mapped et mapped_column→ Types et contraintes+ 2 autres leçons
04
Session et transactions
→ Session pattern→ Transactions et savepoints+ 1 autres leçons
05
Requêtes avancées
→ Joins, CTE, sous-requetes→ Aggregations et analytics+ 1 autres leçons
06
Pydantic v2
→ BaseModel et Field→ Validators et computed fields+ 1 autres leçons
07
SQLAlchemy + Pydantic
→ Mapper SQLAlchemy <-> Pydantic→ Pydantic Settings
08
Migrations et tests
→ Migrations Alembic→ Tests avec pytest et factories+ 1 autres leçons
🏁
Projet final (+ 1 chapitres en chemin)
→ Tu repars avec un projet concret et démontrable

Factories avec factory-boy

NOTEObjectif — Remplacer les donnees de test ecrites a la main par des factories declaratives. Vous allez generer des objets SQLAlchemy realistes, coherents et reproductibles avec factory-boy et Faker, sans dupliquer du code dans chaque test.

Objectifs pedagogiques

TIPA l'issue de ce module
  • Expliquer pourquoi les fixtures ecrites a la main deviennent ingerables
  • Definir une SQLAlchemyModelFactory liee a une session
  • Utiliser Faker, Sequence et SubFactory pour des donnees realistes
  • Gerer les relations (one-to-many) via RelatedFactoryList
  • Brancher les factories sur les fixtures pytest du module precedent

L'intuition : une usine plutot qu'un formulaire vide

Dans un test, vous avez besoin d'un objet User valide. Ecrit a la main, cela donne User(name="Test", email="a@b.c", age=30, is_active=True, ...). Repete dans 40 tests, c'est 40 fois la meme corvee, et le jour ou vous ajoutez une colonne NOT NULL, les 40 tests cassent.

Une factory est une usine : vous decrivez une seule fois comment fabriquer un User typique, puis chaque test demande UserFactory() et recoit un objet complet et valide. Si un test a besoin d'un cas particulier, il surcharge juste le champ concerne : UserFactory(age=17). Tout le reste est rempli automatiquement.

Declaratif

On decrit le modele type, pas chaque instance. Le code de test reste court et lisible.

Realiste

Faker genere noms, emails et adresses plausibles plutot que "aaa".

Reproductible

On peut fixer la graine (seed) pour obtenir les memes donnees a chaque run.

Installation et premiere factory

On installe factory-boy (qui embarque Faker) :

WARNINGAttention au N+1 de creationRelatedFactoryList avec size=100 insere 100 objets un par un. Pour de gros volumes de seed, preferez create_batch ou une insertion en masse.

Brancher les factories sur pytest

On reutilise la fixture db_session du module precedent (rollback automatique). On injecte cette session dans les factories, puis on expose des fixtures pratiques.

Requetes complexes et reporting

NOTEObjectif — Construire la couche analytique de l'ERP : chiffre d'affaires par client, classement des produits, evolution mensuelle. Vous combinez les requetes avancees du Chapitre 04 avec les modeles du projet, et vous exposez les resultats via des schemas Pydantic dedies au reporting.

Objectifs pedagogiques

TIPA l'issue de ce module
  • Ecrire une agregation multi-tables (clients, commandes, lignes) avec GROUP BY
  • Classer des produits avec une window function rank()
  • Structurer une requete lisible avec une CTE
  • Mapper un resultat heterogene (Row) vers un schema Pydantic de reporting
  • Valider ces requetes par des tests sur donnees factory

L'intuition : separer le transactionnel de l'analytique

Jusqu'ici, l'ERP cree des commandes (transactionnel). Le reporting repond a d'autres questions : qui sont mes meilleurs clients ? quel produit se vend le mieux ce mois-ci ? Ces requetes ne modifient rien ; elles agregent et classent. On les isole dans un module reporting/ pour ne pas melanger les responsabilites.

Services transactionnels

Creent et modifient des entites (passer commande, decrementer le stock). Une session, une transaction.

Requetes de reporting

Lecture seule, agregations lourdes. Renvoient des lignes (Row) plutot que des entites ORM.

Chiffre d'affaires par client

On joint Customer, Order et OrderLine, puis on somme quantity * unit_price. On utilise l'API select() 2.0 et func pour l'agregation.

Projet ERP - Modeles

Projet final • Cahier des charges • modeles ORM

NOTEObjectif — Concevoir un mini-ERP : clients, produits, commandes, factures, stock.

Specifications

NOTEFonctionnalites :
  • Gerer clients (B2B/B2C) avec adresses
  • Catalogue produits avec stock multi-entrepot
  • Commandes multi-lignes avec statut workflow
  • Facturation automatique avec TVA
  • Mouvements de stock auditees
  • Rapports par mois / par client / par produit

Structure du projet

output
erp/
├── alembic.ini
├── migrations/
├── app/
│   ├── core/
│   │   ├── config.py            # Settings
│   │   └── database.py
│   ├── models/
│   │   ├── customer.py
│   │   ├── product.py
│   │   ├── order.py
│   │   └── invoice.py
│   ├── schemas/                 # Pydantic In/Out
│   ├── services/                # Logique metier
│   ├── api/                     # FastAPI routes
│   └── main.py
└── tests/

Modele Customer

output
import enum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from datetime import datetime

class CustomerType(str, enum.Enum):
    B2B = "b2b"
    B2C = "b2c"

class Customer(Base):
    __tablename__ = "customers"
    id: Mapped[int] = mapped_column(primary_key=True)
    type: Mapped[CustomerType]
    name: Mapped[str]
    email: Mapped[str] = mapped_column(unique=True)
    tax_id: Mapped[str | None]              # pour B2B
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    
    addresses: Mapped[list["Address"]] = relationship(back_populates="customer", cascade="all")
    orders: Mapped[list["Order"]] = relationship(back_populates="customer")

class Address(Base):
    __tablename__ = "addresses"
    id: Mapped[int] = mapped_column(primary_key=True)
    customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
    street: Mapped[str]
    city: Mapped[str]
    zip_code: Mapped[str]
    country: Mapped[str]
    is_billing: Mapped[bool] = mapped_column(default=False)
    is_shipping: Mapped[bool] = mapped_column(default=True)
    
    customer: Mapped["Customer"] = relationship(back_populates="addresses")

Modele Product et Stock

output
from decimal import Decimal

class Product(Base):
    __tablename__ = "products"
    id: Mapped[int] = mapped_column(primary_key=True)
    sku: Mapped[str] = mapped_column(unique=True)
    name: Mapped[str]
    price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    vat_rate: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("20.00"))
    active: Mapped[bool] = mapped_column(default=True)
    
    stocks: Mapped[list["Stock"]] = relationship(back_populates="product")

class Warehouse(Base):
    __tablename__ = "warehouses"
    id: Mapped[int] = mapped_column(primary_key=True)
    code: Mapped[str] = mapped_column(unique=True)
    name: Mapped[str]

class Stock(Base):
    __tablename__ = "stocks"
    id: Mapped[int] = mapped_column(primary_key=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    warehouse_id: Mapped[int] = mapped_column(ForeignKey("warehouses.id"))
    quantity: Mapped[int] = mapped_column(default=0)
    
    __table_args__ = (
        UniqueConstraint("product_id", "warehouse_id"),
        CheckConstraint("quantity >= 0"),
    )
    
    product: Mapped["Product"] = relationship(back_populates="stocks")

Modele Order et OrderLine

output
class OrderStatus(str, enum.Enum):
    DRAFT = "draft"
    CONFIRMED = "confirmed"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class Order(Base):
    __tablename__ = "orders"
    id: Mapped[int] = mapped_column(primary_key=True)
    customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
    reference: Mapped[str] = mapped_column(unique=True)
    status: Mapped[OrderStatus] = mapped_column(default=OrderStatus.DRAFT)
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    
    customer: Mapped["Customer"] = relationship(back_populates="orders")
    lines: Mapped[list["OrderLine"]] = relationship(back_populates="order", cascade="all, delete-orphan")
    invoice: Mapped["Invoice"] = relationship(back_populates="order", uselist=False)

class OrderLine(Base):
    __tablename__ = "order_lines"
    id: Mapped[int] = mapped_column(primary_key=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"))
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    quantity: Mapped[int] = mapped_column()
    unit_price: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    
    order: Mapped["Order"] = relationship(back_populates="lines")
    product: Mapped["Product"] = relationship()

Modele Invoice et StockMovement

output
class Invoice(Base):
    __tablename__ = "invoices"
    id: Mapped[int] = mapped_column(primary_key=True)
    order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), unique=True)
    number: Mapped[str] = mapped_column(unique=True)
    issued_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
    subtotal: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    vat: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    total: Mapped[Decimal] = mapped_column(Numeric(10, 2))
    paid: Mapped[bool] = mapped_column(default=False)
    
    order: Mapped["Order"] = relationship(back_populates="invoice")

class StockMovement(Base):
    __tablename__ = "stock_movements"
    id: Mapped[int] = mapped_column(primary_key=True)
    product_id: Mapped[int] = mapped_column(ForeignKey("products.id"))
    warehouse_id: Mapped[int] = mapped_column(ForeignKey("warehouses.id"))
    quantity: Mapped[int]                            # negatif = sortie
    reason: Mapped[str]                              # "order_123", "restock"
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)

Resume

NOTEA retenir
  • Modeles separes par domaine (customer, order, product)
  • Decimal pour l'argent (jamais float)
  • Enum pour les statuts workflow
  • cascade pour les aggregations (Order -> lines)
  • StockMovement = audit log immutable
va-plus-loin

Cet article couvre les extraits les plus utiles — le cours complet Python SQLAlchemy Pydantic (9 chapitres, 27 leçons, exercices corrigés et projet final) t'emmène jusqu'au bout.

./acceder-au-cours-complet cours gratuit : Vibe Coding

FAQ

Combien de temps pour apprendre Python SQLAlchemy Pydantic ?
Avec une progression structurée (9 chapitres, 27 leçons courtes et pratiques), on atteint un niveau opérationnel en quelques semaines à raison de 30 à 60 minutes par jour. L'important est de pratiquer chaque notion immédiatement.
Faut-il des prérequis ?
Des bases en informatique suffisent. Si tu sais utiliser un terminal et lire du code simple, tu es prêt.
Par où commencer concrètement ?
Reproduis les commandes de cet article, puis suis le cours complet Python SQLAlchemy Pydantic : il enchaîne les 27 leçons dans l'ordre, avec exercices et projet final.

📬 Tu veux recevoir ce type de guide chaque semaine ? Abonne-toi gratuitement — code réel, zéro blabla.