
SQLAlchemy est l'ORM Python de référence pour dialoguer avec une base de données relationnelle. Un ORM (Object-Relational Mapping) fait correspondre une table à une classe et une ligne à un objet : vous manipulez vos données comme des objets Python au lieu d'écrire du SQL à la main. Ce guide part de zéro et couvre l'installation, la définition de modèles typés en syntaxe SQLAlchemy 2.0, les opérations CRUD et les relations entre tables, sur une base SQLite sans configuration.
Il s'adresse aux développeurs qui découvrent les bases de données en Python et veulent une structure de projet propre, réutilisable ensuite avec FastAPI ou Connexion. Tout le code de cette page a été exécuté avec SQLAlchemy 2.0. La seconde partie enchaîne sur les migrations avec Alembic.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Installer SQLAlchemy et le driver adapté à votre base de données.
- Définir des modèles typés avec la syntaxe moderne
Mapped/mapped_column. - Structurer un projet SQLAlchemy clair et évolutif.
- Créer, lire, modifier et supprimer des données (CRUD) avec
select(). - Modéliser des relations entre tables et comprendre la session.
- Écrire des requêtes sûres, protégées contre les injections SQL.
Qu'est-ce qu'un ORM et pourquoi l'utiliser
Section intitulée « Qu'est-ce qu'un ORM et pourquoi l'utiliser »Un ORM est une couche qui traduit vos objets Python en lignes SQL, et inversement. Sans ORM, vous écrivez des requêtes INSERT, SELECT ou UPDATE sous forme de chaînes de caractères, puis vous convertissez à la main les résultats en objets. C'est répétitif et propice aux erreurs, notamment aux injections SQL quand on concatène des valeurs.
Avec SQLAlchemy, vous décrivez une table par une classe et vous laissez la bibliothèque générer le SQL. Le gain est double : un code plus lisible (on pense en objets métier) et des requêtes paramétrées par défaut, donc plus sûres. Autre avantage décisif : SQLAlchemy s'adapte à la plupart des moteurs, ce qui permet de développer sur SQLite puis de passer en production sur PostgreSQL ou MySQL sans réécrire votre logique.
ORM ou Core : deux niveaux d'abstraction
Section intitulée « ORM ou Core : deux niveaux d'abstraction »SQLAlchemy propose deux approches complémentaires. Bien les distinguer évite de se perdre dans la documentation.
| Couche | Vous manipulez | Cas d'usage typique |
|---|---|---|
| ORM | des objets Python (classes, sessions) | applications, CRUD, relations métier |
| Core | des expressions SQL (Table, select()) | requêtes très optimisées, ETL, SQL complexe |
L'ORM est bâti au-dessus de Core. Pour l'immense majorité des projets, l'ORM suffit et reste plus clair : c'est l'approche que suit ce guide. On descend vers Core uniquement quand on a besoin d'un contrôle précis sur le SQL généré. Les deux couches produisent des requêtes paramétrées, la protection contre les injections est donc acquise dans les deux cas.
Installer SQLAlchemy
Section intitulée « Installer SQLAlchemy »L'installation se fait dans un environnement virtuel, pour isoler les dépendances de votre projet. SQLite est intégré à Python : aucun driver supplémentaire n'est nécessaire pour débuter.
pip install sqlalchemyuv add sqlalchemyPour une autre base que SQLite, ajoutez le driver correspondant au moteur. En 2026, le driver PostgreSQL moderne est psycopg (version 3), qui remplace l'ancien psycopg2 :
pip install "sqlalchemy" "psycopg[binary]" # PostgreSQLpip install "sqlalchemy" pymysql # MySQL / MariaDBVérifiez que tout est en place. La sortie doit afficher une version commençant par 2. :
import sqlalchemyprint(sqlalchemy.__version__) # 2.0.51Définir vos premiers modèles
Section intitulée « Définir vos premiers modèles »Depuis SQLAlchemy 2.0, un modèle hérite de DeclarativeBase et déclare ses colonnes avec des annotations typées : Mapped[...] donne le type, mapped_column(...) les options. Cette syntaxe remplace l'ancien couple declarative_base() / Column(Integer, ...), encore accepté mais dépassé. Le typage explicite est vérifiable par des outils comme mypy, ce qui attrape des erreurs avant l'exécution.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase): pass
class User(Base): __tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(index=True) age: Mapped[int | None] = mapped_column(default=None)Base: la classe racine dont héritent tous vos modèles.__tablename__: le nom de la table dans la base.Mapped[int]: une colonne entière non nulle ;Mapped[str | None]la rend nullable.primary_key=True: la clé primaire, auto-incrémentée pour un entier.
Structurer le projet
Section intitulée « Structurer le projet »Un projet lisible sépare la connexion, les modèles et le point d'entrée. Voici la structure utilisée dans ce guide, adaptée à un petit projet évolutif :
Répertoiremy_project/
Répertoiredb/
- init .py
- database.py
Répertoiremodels/
- init .py
- user.py
- main.py
Le fichier db/database.py centralise l'engine (la connexion) et la fabrique de sessions. C'est aussi ici que vit la classe Base, pour éviter les imports circulaires.
from sqlalchemy import create_enginefrom sqlalchemy.orm import DeclarativeBase, sessionmaker
DATABASE_URL = "sqlite:///example.db"engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
class Base(DeclarativeBase): passengine: gère la connexion à la base et le pool de connexions.echo=False: passez àTrueen développement pour voir le SQL généré. LaissezFalseen production, car les logs SQL peuvent exposer des données.SessionLocal: la fabrique qui produit les sessions, unités de travail sur la base.expire_on_commit=False: garde les objets utilisables après uncommit, pratique pour les afficher juste après.
Le modèle User importe cette Base :
from sqlalchemy.orm import Mapped, mapped_column
from db.database import Base
class User(Base): __tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(index=True) age: Mapped[int | None] = mapped_column(default=None)Le fichier models/__init__.py réexporte le modèle pour des imports courts :
from .user import User
__all__ = ["User"]Créer les tables
Section intitulée « Créer les tables »Avec les modèles définis, une seule ligne demande à SQLAlchemy de créer les tables manquantes à partir de leurs métadonnées :
from db.database import Base, enginefrom models import User # importe le modèle pour l'enregistrer sur Base
if __name__ == "__main__": print("Création des tables...") Base.metadata.create_all(bind=engine) print("Tables créées.")À l'exécution, le fichier example.db apparaît dans le dossier courant et contient la table users :
python main.pyCréation des tables...Tables créées.Les opérations CRUD
Section intitulée « Les opérations CRUD »Le CRUD (Create, Read, Update, Delete) est le cœur de toute interaction avec une base. En SQLAlchemy 2.0, tout passe par une session ouverte avec un gestionnaire de contexte (with), qui garantit sa fermeture même en cas d'erreur.
Créer des enregistrements
Section intitulée « Créer des enregistrements »On instancie des objets, on les ajoute à la session, puis on valide avec commit() :
from db.database import SessionLocalfrom models import User
def add_users() -> None: with SessionLocal() as session: session.add_all([ User(name="Alice", age=25), User(name="Bob", age=30), ]) session.commit() print("Utilisateurs ajoutés.")Lire des enregistrements
Section intitulée « Lire des enregistrements »La syntaxe 2.0 utilise select() exécuté via session.scalars(), qui renvoie directement les objets. Pour une seule ligne par clé primaire, session.get() est le plus direct :
from sqlalchemy import select
from db.database import SessionLocalfrom models import User
def get_users() -> None: with SessionLocal() as session: for user in session.scalars(select(User).order_by(User.name)): print(f"ID {user.id} | {user.name} | {user.age} ans")
def get_one(user_id: int) -> None: with SessionLocal() as session: user = session.get(User, user_id) print(user.name if user else "introuvable")Modifier et supprimer
Section intitulée « Modifier et supprimer »Pour modifier, on récupère l'objet, on change ses attributs, on valide. Pour supprimer, on appelle session.delete() :
def update_user(user_id: int, new_name: str) -> None: with SessionLocal() as session: user = session.get(User, user_id) if user is None: print(f"Aucun utilisateur {user_id}") return user.name = new_name session.commit()
def delete_user(user_id: int) -> None: with SessionLocal() as session: user = session.get(User, user_id) if user is None: print(f"Aucun utilisateur {user_id}") return session.delete(user) session.commit()En enchaînant création, lecture, mise à jour puis suppression, la sortie confirme le cycle complet :
--- après insert ---ID 1 | Alice | 25 ansID 2 | Bob | 30 ans--- après update de Bob et delete d'Alice ---ID 2 | Bob Martin | 30 ansModéliser les relations entre tables
Section intitulée « Modéliser les relations entre tables »La vraie force d'un ORM apparaît avec les relations. Prenons le cas classique un-à-plusieurs : un utilisateur écrit plusieurs articles. On ajoute une clé étrangère (ForeignKey) côté article et une relationship() de chaque côté pour naviguer entre les objets.
from sqlalchemy import ForeignKeyfrom sqlalchemy.orm import Mapped, mapped_column, relationship
from db.database import Base
class User(Base): __tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(index=True) age: Mapped[int | None] = mapped_column(default=None)
articles: Mapped[list["Article"]] = relationship( back_populates="author", cascade="all, delete-orphan" )
class Article(Base): __tablename__ = "articles"
id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="articles")ForeignKey("users.id"): rattache chaque article à un utilisateur.back_populates: maintient les deux côtés de la relation synchronisés.cascade="all, delete-orphan": supprimer un utilisateur supprime aussi ses articles, ce qui évite des lignes orphelines.
On crée alors un utilisateur et ses articles en une seule opération, SQLAlchemy s'occupe des clés étrangères :
with SessionLocal() as session: alice = User(name="Alice", age=25, articles=[Article(title="Premier billet")]) session.add(alice) session.commit() print(f"{alice.name} a {len(alice.articles)} article(s)") # Alice a 1 article(s)Sessions, transactions et sécurité
Section intitulée « Sessions, transactions et sécurité »La session est une unité de travail : elle regroupe vos opérations et ne les écrit dans la base qu'au commit(). En cas de problème, rollback() annule tout ce qui n'a pas été validé, ce qui protège d'un état incohérent. Le gestionnaire de contexte with SessionLocal() as session: referme automatiquement la session à la fin, y compris si une exception survient.
Côté sécurité, SQLAlchemy vous protège par défaut : les valeurs passées à where() ou aux modèles sont liées (bound parameters), jamais concaténées dans le SQL. Le seul vrai danger vient du SQL brut mal écrit.
from sqlalchemy import text
nom = entree_utilisateur # valeur non fiable
# DANGEREUX : injection SQL possible via f-stringsession.execute(text(f"SELECT * FROM users WHERE name = '{nom}'"))
# CORRECT : paramètre liésession.execute(text("SELECT * FROM users WHERE name = :n"), {"n": nom})La règle tient en une phrase : jamais de f-string pour insérer une valeur dans une requête, toujours des paramètres liés. Avec l'ORM (select(User).where(User.name == nom)), cette protection est automatique.
À retenir
Section intitulée « À retenir »- Un ORM fait correspondre classe et table, objet et ligne : on manipule des objets Python plutôt que du SQL.
- SQLAlchemy offre deux niveaux : l'ORM (objets, la voie par défaut) et Core (expressions SQL, pour les cas pointus).
- La syntaxe 2.0 repose sur
DeclarativeBase,Mappedetmapped_column: typée et vérifiable par mypy. - On lit avec
select()+session.scalars();session.query()est legacy. - Les relations (
ForeignKey+relationship) et le cascade évitent le SQL de jointure manuel et les lignes orphelines. - La session encadre une transaction :
commit()valide,rollback()annule. - SQLAlchemy paramètre les requêtes par défaut : ne cassez pas cette protection avec des f-strings dans du SQL brut.
create_allne fait pas évoluer un schéma existant : pour cela, on utilise Alembic.
FAQ : SQLAlchemy et l'ORM Python
Section intitulée « FAQ : SQLAlchemy et l'ORM Python »- une table devient une classe Python,
- une ligne devient un objet,
- une colonne devient un attribut.
pip install sqlalchemy
SQLite fonctionne sans driver supplémentaire (il est inclus dans Python). Pour les autres bases, ajoutez le driver correspondant :pip install "sqlalchemy" "psycopg[binary]" # PostgreSQL
pip install "sqlalchemy" pymysql # MySQL / MariaDB
Vérifiez l'installation :import sqlalchemy
print(sqlalchemy.__version__) # 2.x.x
Bonne pratique : épinglez la version (sqlalchemy==2.0.51) dans votre requirements.txt ou pyproject.toml pour des installations reproductibles.| Couche | Vous manipulez | Pour quoi |
|---|---|---|
| Core | des expressions SQL (Table, select()) |
requêtes optimisées, SQL complexe, ETL |
| ORM | des objets Python (classes, sessions) | applications, CRUD, relations |
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(index=True)
age: Mapped[int | None] = mapped_column(default=None)
DeclarativeBaseremplace l'anciendeclarative_base().Mapped[int]donne le type de la colonne, vérifié par mypy.Mapped[str | None]signale une colonne nullable.
Column(Integer, ...)), encore acceptée mais dépassée.select() exécutée via la session :from sqlalchemy import select
with SessionLocal() as session:
# tous les majeurs, triés par nom
users = session.scalars(
select(User).where(User.age >= 18).order_by(User.name)
).all()
# un seul par clé primaire
alice = session.get(User, 1)
session.scalars(select(...))renvoie directement les objets.session.get(User, id)récupère une ligne par sa clé primaire.
session.query(User) fonctionne encore mais est considérée legacy en 2.0 : préférez select() sur les nouveaux projets.# sûr : la valeur est un paramètre lié
session.scalars(select(User).where(User.name == nom_saisi))
Le risque apparaît si vous écrivez du SQL brut avec des chaînes formatées :from sqlalchemy import text
# DANGEREUX : injection SQL possible
session.execute(text(f"SELECT * FROM users WHERE name = '{nom_saisi}'"))
# correct : paramètre lié
session.execute(text("SELECT * FROM users WHERE name = :n"), {"n": nom_saisi})
Règle : jamais de f-string pour insérer une valeur dans du SQL. Toujours des paramètres liés.Prochaines étapes
Section intitulée « Prochaines étapes »Votre base vit et votre schéma va évoluer. La suite logique est de versionner ces changements avec Alembic, puis d'exposer vos données via une API.