Aller au contenu
Développement medium

Alembic : gérer les migrations SQLAlchemy en Python

11 min de lecture

logo SQLAlchemy

Alembic est l'outil de migration de schéma officiel de SQLAlchemy. Il versionne l'évolution de votre base de données : chaque changement de modèle (ajout de colonne, nouvelle table) devient un script de migration horodaté, applicable avec upgrade et réversible avec downgrade. Vous faites ainsi évoluer une base en production sans la recréer ni perdre de données.

Ce guide fait suite à la première partie sur SQLAlchemy et se concentre sur la pratique : installer Alembic, l'initialiser dans un projet, générer des migrations automatiquement, les appliquer, les annuler et adopter les bons réflexes en équipe. Tout le cycle a été exécuté avec Alembic 1.18 sur une base SQLite.

  • Installer et initialiser Alembic dans un projet SQLAlchemy existant.
  • Relier Alembic à vos modèles via env.py et alembic.ini.
  • Générer une migration automatiquement avec --autogenerate.
  • Appliquer, annuler et inspecter les migrations (upgrade, downgrade, history).
  • Éviter les pièges de l'autogénération et sécuriser les migrations en production.

Une migration est un changement contrôlé du schéma d'une base : ajouter une colonne, créer une table, modifier un type. Le problème, c'est que Base.metadata.create_all() de SQLAlchemy ne crée que les tables absentes et n'altère jamais l'existant. Dès qu'une base contient des données réelles, recréer les tables n'est plus envisageable : il faut modifier le schéma en place, sans rien perdre.

Alembic répond exactement à ce besoin. Écrit par l'auteur de SQLAlchemy, il transforme chaque évolution en un script versionné que vous stockez dans Git à côté de votre code. Chaque environnement (poste de développement, préproduction, production) applique la même suite de migrations dans le même ordre, ce qui garantit des schémas identiques partout. En cas d'erreur, vous disposez d'un chemin de retour explicite.

Avant de commencer, vous devez avoir :

Alembic s'installe via pip. Il tire SQLAlchemy comme dépendance, mais autant l'installer explicitement :

Fenêtre de terminal
pip install alembic sqlalchemy

Vérifiez la version. La sortie doit afficher un numéro 1.x :

Fenêtre de terminal
alembic --version
# alembic 1.18.5

Pour illustrer, partons d'un modèle minimal dans un fichier models.py, en syntaxe SQLAlchemy 2.0 :

models.py
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(nullable=False)

C'est ce modèle qu'Alembic va comparer à la base pour détecter les changements. Notez que nous ne créons pas la table à la main : c'est désormais le rôle des migrations.

À la racine du projet, une seule commande met en place toute la structure :

Fenêtre de terminal
alembic init alembic

Elle crée un dossier alembic/ et un fichier de configuration :

  • Répertoiremon_projet/
    • Répertoirealembic/
      • Répertoireversions/
      • env.py
      • script.py.mako
      • README
    • alembic.ini
    • models.py

Trois éléments comptent :

  • alembic.ini : la configuration principale, dont l'URL de la base.
  • alembic/env.py : le script qui relie Alembic à vos modèles.
  • alembic/versions/ : le dossier où seront écrits les scripts de migration.

Alembic ne connaît ni votre base ni vos modèles tant que vous ne les lui indiquez pas. Deux réglages sont nécessaires.

  1. Indiquer l'URL de la base dans alembic.ini. Repérez la ligne sqlalchemy.url et renseignez votre base SQLite :

    sqlalchemy.url = sqlite:///example.db
  2. Pointer Alembic vers vos modèles dans alembic/env.py. Remplacez la ligne target_metadata = None par l'import de votre Base :

    from models import Base
    target_metadata = Base.metadata
  3. Activer le mode batch pour SQLite. Toujours dans env.py, ajoutez render_as_batch=True à l'appel context.configure de la fonction run_migrations_online :

    with connectable.connect() as connection:
    context.configure(
    connection=connection,
    target_metadata=target_metadata,
    render_as_batch=True,
    )

Alembic sait maintenant comparer vos modèles à l'état réel de la base. La commande --autogenerate produit le script correspondant :

Fenêtre de terminal
alembic revision --autogenerate -m "initial"

La sortie confirme la détection de la table :

INFO [alembic.autogenerate.compare.tables] Detected added table 'users'
Generating .../alembic/versions/7cac93520a2c_initial.py ... done

Le script généré dans alembic/versions/ décrit la création de la table. La fonction upgrade() applique le changement, downgrade() l'annule :

"""initial
Revision ID: 7cac93520a2c
Revises:
Create Date: 2026-07-01 19:34:52
"""
from alembic import op
import sqlalchemy as sa
revision = "7cac93520a2c"
down_revision = None
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
def downgrade() -> None:
op.drop_table("users")

Pour synchroniser la base avec le script, appliquez toutes les migrations jusqu'à la plus récente (head) :

Fenêtre de terminal
alembic upgrade head
INFO [alembic.runtime.migration] Running upgrade -> 7cac93520a2c, initial

La table users existe désormais, et Alembic a créé une table technique alembic_version qui mémorise la révision courante de la base.

Le vrai intérêt d'Alembic apparaît quand le modèle change. Ajoutons un champ email à User :

class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
email: Mapped[str | None] = mapped_column(default=None) # nouveau champ

On génère une nouvelle migration. Alembic détecte la colonne ajoutée :

Fenêtre de terminal
alembic revision --autogenerate -m "add email"
INFO [alembic.autogenerate.compare.tables] Detected added column 'users.email'

Grâce au mode batch, le script utilise op.batch_alter_table, la forme compatible SQLite :

def upgrade() -> None:
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.add_column(sa.Column("email", sa.String(), nullable=True))
def downgrade() -> None:
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_column("email")

Un alembic upgrade head applique la modification. La colonne email est bien présente dans la table users.

Si une migration pose problème, downgrade exécute sa fonction downgrade() pour revenir en arrière :

Fenêtre de terminal
alembic downgrade -1 # annule la dernière migration
alembic downgrade 7cac93520a2c # revient à une révision précise
alembic downgrade base # revient à une base vide

Après un downgrade -1 sur notre exemple, la colonne email disparaît et la base retrouve son schéma précédent.

Deux commandes donnent une vue claire de la situation :

Fenêtre de terminal
alembic history # la chaîne complète des révisions
alembic current # la révision appliquée sur la base

La sortie de history montre l'enchaînement des révisions, de la base jusqu'au head :

7cac93520a2c -> 5202795bdd97 (head), add email
<base> -> 7cac93520a2c, initial

Alembic est puissant, mais quelques réflexes évitent les incidents en équipe et en production.

  • Relire chaque script autogénéré. L'autogénération est une aide, pas une vérité : elle rate les renommages et certains types. La revue est obligatoire.
  • Ne jamais autogénérer contre la production. On génère et on relit en développement, on versionne dans Git, puis la production n'exécute que alembic upgrade head, idéalement depuis la CI/CD.
  • Sauvegarder avant de migrer une base réelle. Une migration destructrice sans sauvegarde est irréversible.
  • Sortir l'URL de production du fichier. Ne stockez pas l'URL de la base de production en clair dans alembic.ini : lisez-la depuis une variable d'environnement dans env.py (os.environ["DATABASE_URL"]).
  • Nommer les migrations clairement (-m "add email field to users") et les committer avec le code applicatif correspondant, pour que chaque déploiement reste cohérent.
  • Résoudre les conflits de branches. Quand deux personnes créent une migration en parallèle, deux révisions partagent le même parent. alembic merge crée une révision de fusion pour réunir les deux branches.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Une migration modifie le schéma en place, sans recréer la base ni perdre les données.
  • Alembic versionne ces changements en scripts stockés dans Git, appliqués à l'identique dans chaque environnement.
  • Le cycle de base : alembic init, configurer alembic.ini et env.py, puis revision --autogenerate et upgrade head.
  • render_as_batch=True est indispensable sur SQLite pour modifier une colonne existante.
  • L'autogénération relit obligatoirement : un renommage mal détecté efface des données.
  • En production, on applique seulement des migrations relues et testées, après sauvegarde ; le downgrade est rarement sûr.
  • alembic history et alembic current donnent l'état des révisions.
  • PostgreSQL : passer de SQLite à une base de production robuste.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn