À quoi ça sert

Quand ton code Python a besoin de stocker des données durablement, on utilise une base de données. Sans ORM, tu écrirais du SQL à la main :

python (sans ORM)
cursor.execute("INSERT INTO users (nom, email) VALUES (?, ?)", ("Alice", "a@b.com"))
cursor.execute("SELECT * FROM users WHERE id = ?", (1,))
row = cursor.fetchone()
nom = row[1]   # bonne chance pour te souvenir des index

Avec SQLAlchemy en mode ORM, tu manipules des objets :

python (avec ORM)
session.add(User(nom="Alice", email="a@b.com"))
session.commit()

user = session.get(User, 1)
print(user.nom)        # attribut Python normal

Ce que SQLAlchemy t'apporte concrètement :

  • Indépendance du moteur — tu changes de SQLite à PostgreSQL en changeant une seule chaîne de connexion.
  • Sécurité — les valeurs sont automatiquement échappées, fini les injections SQL.
  • Typage — tes modèles sont des classes Python typées, l'auto-complétion fonctionne partout.
  • Migrations — couplé à Alembic, tu versionnes le schéma de ta BD comme ton code.
  • Deux modes — Core (proche du SQL, plus de contrôle) ou ORM (haut niveau, plus rapide à écrire).
En une phrase

SQLAlchemy te laisse écrire du Python idiomatique pendant qu'il s'occupe de générer le SQL correspondant.

Un exemple d'usage

Ton API FastAPI doit stocker une liste de livres. Sans serveur de base à monter, tu commences avec SQLite (un simple fichier .db). Plus tard, tu passes à PostgreSQL en changeant une ligne.

python
from sqlalchemy import create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase): pass

class Livre(Base):
    __tablename__ = "livres"
    id: Mapped[int] = mapped_column(primary_key=True)
    titre: Mapped[str]
    auteur: Mapped[str]

engine = create_engine("sqlite:///bibli.db")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add(Livre(titre="1984", auteur="Orwell"))
    session.commit()

    for livre in session.scalars(select(Livre)):
        print(livre.titre, "—", livre.auteur)

Aucune ligne de SQL écrite à la main. Et le fichier bibli.db est créé automatiquement.

How-to : installer et utiliser SQLAlchemy

  1. Installer SQLAlchemy

    Avec UV (voir la fiche UV) :

    bash
    uv add "sqlalchemy>=2.0"

    SQLite est inclus dans Python, pas de driver à installer pour démarrer. Pour PostgreSQL ou MySQL, ajoute le driver correspondant :

    bash
    # PostgreSQL
    uv add "psycopg[binary]"
    
    # MySQL / MariaDB
    uv add pymysql
    
    # Pour les versions async
    uv add aiosqlite        # SQLite async
  2. Connecter à une base de données

    La connection string (URL) est la seule chose qui change selon le moteur. Elle suit le format dialecte+driver://user:password@host/db.

    python
    from sqlalchemy import create_engine
    
    # SQLite (fichier local)
    engine = create_engine("sqlite:///app.db", echo=True)
    
    # PostgreSQL
    engine = create_engine("postgresql+psycopg://user:pwd@localhost:5432/madb")
    
    # MySQL
    engine = create_engine("mysql+pymysql://user:pwd@localhost/madb")
    Astuce — echo

    echo=True affiche le SQL généré dans la console. Indispensable pour comprendre ce que SQLAlchemy fait sous le capot. À retirer en prod.

  3. Définir tes modèles

    SQLAlchemy 2.0 utilise une syntaxe typée avec Mapped :

    python
    from datetime import datetime
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
    from sqlalchemy import ForeignKey
    
    class Base(DeclarativeBase): pass
    
    class Auteur(Base):
        __tablename__ = "auteurs"
        id: Mapped[int] = mapped_column(primary_key=True)
        nom: Mapped[str] = mapped_column(unique=True)
    
        livres: Mapped[list["Livre"]] = relationship(back_populates="auteur")
    
    class Livre(Base):
        __tablename__ = "livres"
        id: Mapped[int] = mapped_column(primary_key=True)
        titre: Mapped[str]
        publication: Mapped[datetime] = mapped_column(default=datetime.utcnow)
        auteur_id: Mapped[int] = mapped_column(ForeignKey("auteurs.id"))
    
        auteur: Mapped["Auteur"] = relationship(back_populates="livres")
  4. Créer les tables

    Une seule fois (ou à chaque démarrage en dev) :

    python
    Base.metadata.create_all(engine)

    En prod, on ne fait pas ça : on utilise Alembic pour gérer les migrations (voir plus bas).

  5. Ouvrir une session et faire des opérations

    La session est ton interface principale. Elle gère la transaction et le cache des objets.

    python
    from sqlalchemy.orm import Session
    from sqlalchemy import select
    
    with Session(engine) as session:
        # Insérer
        orwell = Auteur(nom="George Orwell")
        session.add_all([
            orwell,
            Livre(titre="1984", auteur=orwell),
            Livre(titre="La Ferme des animaux", auteur=orwell),
        ])
        session.commit()
    
        # Lire
        livre = session.get(Livre, 1)            # par PK
    
        # Requête plus riche
        stmt = select(Livre).where(Livre.titre.like("%1984%"))
        resultats = session.scalars(stmt).all()
    
        # Jointure implicite via la relation
        auteur = session.scalars(
            select(Auteur).where(Auteur.nom == "George Orwell")
        ).first()
        for l in auteur.livres:
            print(l.titre)
    
        # Modifier
        livre.titre = "1984 (édition spéciale)"
        session.commit()
    
        # Supprimer
        session.delete(livre)
        session.commit()
    Pattern recommandé

    Toujours encadrer une session avec with Session(engine) as session:. Le rollback automatique en cas d'exception et la fermeture propre sont gérés pour toi.

  6. Connecter SQLAlchemy à FastAPI

    Dépendance FastAPI qui ouvre/ferme la session par requête :

    python
    from fastapi import FastAPI, Depends
    from sqlalchemy.orm import Session, sessionmaker
    
    SessionLocal = sessionmaker(engine, expire_on_commit=False)
    
    def get_db():
        with SessionLocal() as db:
            yield db
    
    app = FastAPI()
    
    @app.get("/livres")
    def lister(db: Session = Depends(get_db)):
        return db.scalars(select(Livre)).all()
  7. Versionner le schéma avec Alembic

    En prod, tu n'utilises plus create_all : tu génères des migrations incrémentales. Alembic, créé par le même auteur que SQLAlchemy, est la solution standard.

    bash
    uv add --dev alembic
    uv run alembic init migrations
    
    # Génère un script de migration depuis tes modèles
    uv run alembic revision --autogenerate -m "create users"
    
    # Applique les migrations en attente
    uv run alembic upgrade head
  8. Configurer VS Code

    1. Installe l'extension Python de Microsoft (interpréteur du venv).
    2. Pour explorer la base SQLite directement dans l'éditeur, l'extension SQLite Viewer (qwtel.sqlite-viewer) est très pratique : double-clic sur app.db et tu vois les tables.
    3. Pour PostgreSQL / MySQL, l'extension SQLTools (mtxr.sqltools) avec son driver correspondant.
    Voir le SQL généré

    Mets echo=True dans create_engine pendant le dev : tu vois exactement les requêtes SQL produites par chaque appel SQLAlchemy. Excellent pour comprendre — et pour traquer les requêtes lentes.

Aide-mémoire

python
# Filtres
select(User).where(User.age >= 18)
select(User).where(User.nom.like("A%"))
select(User).where(User.id.in_([1, 2, 3]))

# Tri / pagination
select(User).order_by(User.nom).limit(10).offset(20)

# Agrégations
from sqlalchemy import func
session.scalar(select(func.count(User.id)))

# Récupérer un seul / premier / tous
session.scalar(stmt)         # 1 résultat ou None
session.scalars(stmt).first()
session.scalars(stmt).all()

# Transaction explicite
with session.begin():
    session.add(...)
    # commit auto si pas d'exception

Pour aller plus loin