SQLite reste souvent le meilleur point de départ quand je veux une base locale fiable, légère et facile à déployer avec Python. Dans un projet Python SQLite, je pars d’un principe simple : la base doit rester simple à ouvrir, simple à tester et assez propre pour ne pas devenir un frein dès que l’application grandit. Ici, je vais aller droit aux usages concrets, avec le bon niveau de détail pour créer, lire, mettre à jour et maintenir une base sans mauvaises habitudes.
L’essentiel à retenir avant de coder
- SQLite est un excellent choix pour les prototypes, les scripts, les outils internes et les applications locales.
- Avec Python, le module `sqlite3` suffit souvent sans dépendance externe ni serveur à administrer.
- Les requêtes paramétrées sont non négociables si vous voulez éviter l’injection SQL et garder un code lisible.
- Les transactions doivent être explicites ou, au minimum, contrôlées proprement avec un contexte `with`.
- `sqlite3.Row` simplifie énormément la lecture des résultats quand les tuples deviennent pénibles à manipuler.
- SQLite tient très bien tant que la concurrence d’écriture reste modérée et que le schéma est bien pensé.
Pourquoi SQLite reste un excellent choix avec Python
Ce que j’aime dans SQLite, c’est le rapport entre simplicité et solidité. On obtient un fichier de base de données unique, sans serveur à lancer, sans configuration lourde, et avec un comportement suffisamment prévisible pour des besoins réels. Pour un outil interne, une API légère, une application de bureau ou une base de test, c’est souvent plus rationnel qu’une base serveur plus complexe à opérer.
Le point à comprendre, en revanche, c’est sa zone de confort. SQLite tolère très bien plusieurs lectures simultanées, mais il n’accepte qu’une écriture à la fois. Autrement dit, il est très bon pour des charges modestes ou structurées, et moins adapté quand plusieurs services écrivent en parallèle toute la journée. Ce n’est pas une faiblesse cachée, c’est une limite de conception qu’il faut intégrer dès le départ.
| Cas d’usage | SQLite | Mon avis |
|---|---|---|
| Prototype, script, test automatisé | Excellent | Je le choisis presque sans hésiter. |
| Application locale ou embarquée | Très bon | Le meilleur compromis dans beaucoup de cas. |
| API simple avec peu d’écritures concurrentes | Bon | Oui, si le schéma est propre et les transactions courtes. |
| Plusieurs services écrivant en même temps | Limité | Je commence à regarder PostgreSQL ou MySQL. |
En pratique, je garde SQLite quand la priorité est de livrer vite sans sacrifier la fiabilité, puis je fais évoluer l’architecture seulement quand les besoins réels l’imposent. Une fois ce cadre posé, le vrai sujet devient la structure de la base elle-même.

Construire un schéma propre dès le départ
Le piège classique consiste à se dire que SQLite est “souple”, donc qu’on peut repousser la modélisation. Je fais l’inverse : j’utilise cette souplesse pour prototyper rapidement, mais je pose tout de suite les contraintes qui évitent les données bancales. Un bon schéma vaut mieux qu’une couche Python trop intelligente qui tente de corriger les erreurs après coup.
Voici la structure que je privilégie souvent pour un petit système de contenus ou de notes :
import sqlite3
from pathlib import Path
db_path = Path("app.db")
with sqlite3.connect(db_path, autocommit=False) as con:
con.execute("PRAGMA foreign_keys = ON")
con.execute("""
CREATE TABLE IF NOT EXISTS article (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('draft', 'published')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")Je privilégie ici trois idées simples. D’abord, `INTEGER PRIMARY KEY` suffit dans la plupart des cas pour un identifiant interne. Ensuite, les contraintes `NOT NULL`, `CHECK` et `DEFAULT` font une vraie différence, parce qu’elles déplacent la validation au bon endroit. Enfin, je garde les types lisibles, mais je ne leur demande pas de faire tout le travail de validation à la place de l’application.
Si votre modèle contient des relations, l’activation explicite des clés étrangères mérite de devenir un réflexe. C’est un petit détail qui évite de grosses incohérences plus tard, surtout quand plusieurs parties du code écrivent dans la même base. À partir de là, on peut s’attaquer aux opérations courantes sans bricoler les requêtes.
Insérer et interroger sans exposer l’application
La règle la plus importante ici est simple : je n’assemble jamais une requête SQL avec des morceaux de texte venant de l’extérieur. J’utilise des paramètres, toujours. Avec `sqlite3`, on peut travailler avec des placeholders `?` ou nommés, et c’est ce qui rend le code sûr, lisible et facile à maintenir.
import sqlite3
notes = [
("Structure", "Définir le schéma avant les requêtes", "draft"),
("Sécurité", "Utiliser des paramètres, jamais de concaténation", "published"),
]
with sqlite3.connect("app.db", autocommit=False) as con:
con.execute("PRAGMA foreign_keys = ON")
con.executemany(
"INSERT INTO article(title, body, status) VALUES(?, ?, ?)",
notes,
)
drafts = con.execute(
"SELECT id, title, status FROM article WHERE status = ? ORDER BY id DESC",
("draft",),
).fetchall()
con.execute(
"UPDATE article SET status = ? WHERE title = ?",
("published", "Structure"),
)
con.execute(
"DELETE FROM article WHERE status = ? AND title = ?",
("draft", "Ancien brouillon"),
)J’utilise `executemany()` dès que j’ai plusieurs lignes à insérer, parce que cela garde le code compact et évite de répéter la même requête. Pour les recherches, je préfère `fetchall()` uniquement si le résultat reste raisonnable ; sinon, j’itère directement sur le curseur pour ne pas charger inutilement la mémoire.
Quand une requête devient plus longue, les paramètres nommés comme `:title` ou `:status` sont souvent plus confortables à lire que des positions multiples. Le gain n’est pas esthétique seulement : sur des requêtes à cinq ou six filtres, on réduit vraiment les erreurs de permutation. La suite logique, c’est de voir comment traiter les résultats et les transactions sans se tromper de niveau d’abstraction.
Traiter les résultats et les transactions proprement
Par défaut, `sqlite3` renvoie des tuples. C’est acceptable pour des scripts courts, mais je passe vite à `sqlite3.Row` dès que l’application s’épaissit, parce que l’accès par nom de colonne reste plus parlant. Ce choix évite aussi une bonne partie des index magiques du style `row[3]`, qui vieillissent mal.
import sqlite3
with sqlite3.connect("app.db", autocommit=False) as con:
con.row_factory = sqlite3.Row
for row in con.execute("SELECT id, title, status FROM article ORDER BY id DESC"):
print(row["title"], row["status"])Je fais aussi attention au comportement transactionnel. Avec un contexte `with`, la connexion commit ou rollback les opérations ouvertes quand on sort du bloc, ce qui protège bien les modifications groupées. En revanche, ce contexte ne ferme pas la connexion à votre place, donc je ferme explicitement quand le flux du programme le demande, ou j’utilise un gestionnaire dédié si le cycle de vie est plus long.
| Mode | Usage | Mon conseil |
|---|---|---|
| `autocommit=False` | Transactions explicites, code moderne | C’est le choix que je privilégie pour un nouveau projet. |
| `autocommit=True` | Mode autocommit bas niveau | Utile dans des scripts très ciblés, pas comme réflexe général. |
| Contrôle hérité via `isolation_level` | Compatibilité avec du code ancien | Je l’évite dans du code neuf si je peux faire autrement. |
En bref, je veux des transactions courtes, visibles et faciles à relire. C’est la meilleure défense contre les écritures partielles, et c’est aussi ce qui prépare le terrain pour les gains de performance sans suringénierie.
Optimiser sans surcorriger
Quand une base SQLite commence à ralentir, mon premier réflexe n’est pas de changer d’outil, mais de regarder la requête, les index et le volume réel. Beaucoup de problèmes viennent d’un mauvais filtrage, d’un schéma trop flou ou d’un usage trop large des lectures. Un index bien placé sur une colonne souvent utilisée dans `WHERE` ou `JOIN` fait souvent plus qu’un réglage agressif de la base.
Je garde aussi en tête quelques limites très concrètes. La connexion Python est, par défaut, protégée contre l’usage depuis un autre thread, et c’est une bonne chose : si je dois partager une base entre threads, je préfère une connexion par thread ou une architecture qui évite le partage direct. Je regarde également les requêtes avec `EXPLAIN QUERY PLAN` avant de toucher à des options plus spécifiques, parce qu’un bon plan d’exécution vaut mieux qu’un réglage appliqué à l’aveugle.
- Sur les lectures lentes, je vérifie d’abord les index et la forme de la requête.
- Sur les écritures concurrentes, je réduis la durée des transactions et je limite le nombre de points d’écriture.
- Sur les réglages SQLite, je teste chaque `PRAGMA` sur un environnement réel, car un `PRAGMA` inconnu peut être ignoré sans alerte utile.
- Sur les threads, je n’improvise pas : la concurrence mal gérée coûte plus cher qu’elle ne rapporte.
Il m’arrive aussi de considérer des options comme le mode WAL quand le profil de charge le justifie, mais je ne le traite jamais comme une recette magique. Le bon réglage dépend du type d’accès, pas d’une promesse générique de performance. Une fois ces points clarifiés, la vraie question devient celle du bon outil pour la suite du projet.
Quand je garde SQLite et quand je change d’outil
Je garde SQLite tant que le projet reste simple à opérer et que la base n’a pas vocation à devenir un nœud central distribué. C’est souvent le cas des prototypes solides, des back-offices modestes, des applications locales ou des services qui écrivent peu et lisent beaucoup. Dans ce cadre, la combinaison simplicité-fiabilité est difficile à battre.
Je commence à envisager une base serveur quand plusieurs services écrivent en parallèle de manière régulière, quand j’ai besoin de permissions plus fines, de réplication, d’un vrai outillage d’administration ou d’une montée en charge plus prévisible. À ce stade, le problème n’est pas que SQLite soit “mauvais” ; c’est juste qu’il ne répond plus au bon cahier des charges. Et avant de migrer, je vérifie toujours que le schéma, les index et les transactions ont déjà été nettoyés, parce qu’une bonne modélisation règle parfois la moitié du problème.
Mon critère final est simple : si SQLite me permet de livrer vite, de rester lisible et de garder une base saine, je le conserve. Si le projet dépend d’une forte concurrence d’écriture ou d’une administration plus lourde que la base elle-même, je change d’outil sans regret. Pour beaucoup d’applications Python, SQLite reste une excellente décision, à condition de le traiter comme une vraie base de données et non comme un simple fichier pratique.