Skip to content

Tutoriel Python SQLite3 : guide complet de la base de données SQLite en Python

Updated on

Vous devez stocker des données structurées dans votre application Python. Il peut s’agir de paramètres utilisateur, de mesures de capteurs, d’entrées de logs ou d’une liste de tâches. Vous vous tournez vers un fichier CSV, mais les données grossissent et les requêtes deviennent pénibles. Vous envisagez PostgreSQL ou MySQL, mais mettre en place un serveur de base de données pour un outil local ou un prototype ressemble à un excès de complexité. Il vous faut simplement un moyen fiable de stocker, interroger et mettre à jour des données structurées sans surcharge.

C’est exactement le problème que SQLite résout. SQLite est un moteur de base de données autonome et sans serveur qui stocke tout dans un seul fichier. Python inclut le module sqlite3 dans la bibliothèque standard, donc il n’y a rien à installer. Vous obtenez un support SQL complet, des transactions ACID et la capacité de gérer des jeux de données jusqu’à 281 téraoctets — le tout sans exécuter de processus serveur ni gérer des fichiers de configuration.

Ce guide parcourt tout ce dont vous avez besoin pour utiliser SQLite efficacement en Python : de la création de votre première base de données aux requêtes avancées, à l’intégration avec pandas, au réglage des performances et à un projet complet du monde réel.

📚

Qu’est-ce que SQLite et pourquoi l’utiliser ?

SQLite est un moteur de base de données relationnelle embarqué. Contrairement à PostgreSQL ou MySQL, il ne s’exécute pas comme un processus serveur séparé. Toute la base de données vit dans un seul fichier sur disque (ou en mémoire). Le module sqlite3 de la bibliothèque standard Python fournit une interface conforme à DB-API 2.0 vers SQLite.

Caractéristiques clés de SQLite :

  • Zéro configuration : pas de serveur à mettre en place, pas d’utilisateurs, pas de permissions à gérer
  • Sans serveur : le moteur de base de données s’exécute dans le processus de votre application
  • Fichier unique : toute la base (tables, index, données) tient dans un seul fichier .db
  • Multiplateforme : le fichier de base fonctionne sur n’importe quel OS sans conversion
  • Compatible ACID : support complet des transactions avec commit et rollback
  • Léger : la bibliothèque fait environ 750 KB — plus petit que la plupart des images

Quand utiliser SQLite

Cas d’usageSQLitePostgreSQL/MySQL
Applications desktop/mobileMeilleur choixSurdimensionné
PrototypageMeilleur choixPlus long à mettre en place
Appareils embarqués / IoTMeilleur choixPeu faisable
Tests unitairesMeilleur choixNécessite un serveur de test
Scripts d’analyse de donnéesBon choixSurcharge inutile
Application web avec < 100K visites/jourFonctionne bienPlus scalable
Application web à forte concurrencePas idéalMeilleur choix
Plusieurs clients écrivant intensivementPas idéalMeilleur choix

SQLite couvre la plupart des applications qui ne nécessitent pas plusieurs connexions d’écriture simultanées. Android et iOS utilisent SQLite comme base locale par défaut. Firefox, Chrome et macOS l’utilisent en interne. C’est le moteur de base de données le plus déployé au monde.

Se connecter à une base de données

La fonction sqlite3.connect() crée une connexion à un fichier de base de données. Si le fichier n’existe pas, SQLite le crée automatiquement.

import sqlite3
 
# Connect to a database file (creates it if it doesn't exist)
conn = sqlite3.connect("myapp.db")
 
# Create a cursor to execute SQL statements
cursor = conn.cursor()
 
# Always close the connection when done
conn.close()

Bases de données en mémoire

Utilisez la chaîne spéciale :memory: pour créer une base de données qui n’existe qu’en RAM. C’est idéal pour les tests et le traitement temporaire de données.

import sqlite3
 
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
 
cursor.execute("CREATE TABLE test (id INTEGER, value TEXT)")
cursor.execute("INSERT INTO test VALUES (1, 'hello')")
cursor.execute("SELECT * FROM test")
print(cursor.fetchone())  # (1, 'hello')
 
conn.close()  # Database is gone after this

Utiliser des context managers

Le schéma recommandé utilise un context manager (instruction with) pour s’assurer que la connexion est correctement gérée, même en cas d’erreur. Les connexions SQLite effectuent aussi un auto-commit en cas de succès et un auto-rollback en cas d’exception à l’intérieur d’un bloc with.

import sqlite3
 
# Connection as context manager handles commit/rollback
with sqlite3.connect("myapp.db") as conn:
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
    cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
    # Auto-commits if no exception
    # Auto-rolls back if an exception occurs
 
# Note: the connection is NOT closed by 'with' -- it just commits/rolls back
# For full cleanup, close explicitly or use a wrapper:
conn.close()

Une fonction helper propre qui gère à la fois commit/rollback et la fermeture :

import sqlite3
from contextlib import contextmanager
 
@contextmanager
def get_db(db_path="myapp.db"):
    """Context manager that commits on success, rolls back on error, and always closes."""
    conn = sqlite3.connect(db_path)
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()
 
# Usage
with get_db() as conn:
    conn.execute("INSERT INTO users (name) VALUES (?)", ("Bob",))

Créer des tables

Utilisez CREATE TABLE pour définir le schéma. SQLite prend en charge un système de types simplifié avec cinq classes de stockage.

Types de données SQLite

Classe de stockageDescriptionÉquivalent Python
NULLValeur nulleNone
INTEGEREntier signé (1, 2, 3, 4, 6 ou 8 octets)int
REALFlottant (IEEE 8 octets)float
TEXTChaîne UTF-8 ou UTF-16str
BLOBDonnées binaires, stockées telles quellesbytes

SQLite utilise le typage dynamique. Vous pouvez stocker n’importe quel type dans n’importe quelle colonne, indépendamment du type déclaré. Le type déclaré est un indice, pas une contrainte. Cela diffère de PostgreSQL et MySQL.

Créer une table avec des contraintes

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        department TEXT DEFAULT 'General',
        salary REAL CHECK(salary > 0),
        hire_date TEXT NOT NULL,
        is_active INTEGER DEFAULT 1
    )
""")
 
conn.commit()
conn.close()

Contraintes clés :

  • PRIMARY KEY : identifiant unique pour chaque ligne. INTEGER PRIMARY KEY est un alias du rowid interne.
  • AUTOINCREMENT : garantit que le rowid augmente toujours (ne réutilise jamais les IDs supprimés).
  • NOT NULL : la colonne ne peut pas contenir de valeurs NULL.
  • UNIQUE : aucune deux lignes ne peuvent avoir la même valeur dans cette colonne.
  • DEFAULT : valeur par défaut si rien n’est fourni lors d’un INSERT.
  • CHECK : valide les données selon une condition.

Créer plusieurs tables liées

import sqlite3
 
conn = sqlite3.connect("school.db")
cursor = conn.cursor()
 
# Enable foreign key enforcement (off by default in SQLite)
cursor.execute("PRAGMA foreign_keys = ON")
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS students (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE
    )
""")
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS courses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        credits INTEGER DEFAULT 3
    )
""")
 
cursor.execute("""
    CREATE TABLE IF NOT EXISTS enrollments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER NOT NULL,
        course_id INTEGER NOT NULL,
        grade TEXT,
        enrolled_date TEXT DEFAULT CURRENT_DATE,
        FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
        FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
        UNIQUE(student_id, course_id)
    )
""")
 
conn.commit()
conn.close()

Opérations CRUD

CRUD signifie Create, Read, Update, Delete — les quatre opérations de base pour le stockage persistant.

INSERT : ajouter des données

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Insert a single row
cursor.execute(
    "INSERT INTO employees (name, email, department, salary, hire_date) VALUES (?, ?, ?, ?, ?)",
    ("Alice Johnson", "alice@company.com", "Engineering", 95000, "2024-01-15")
)
 
# Insert multiple rows with executemany
employees = [
    ("Bob Smith", "bob@company.com", "Marketing", 72000, "2024-03-01"),
    ("Carol White", "carol@company.com", "Engineering", 98000, "2023-11-20"),
    ("David Brown", "david@company.com", "Sales", 68000, "2024-06-10"),
    ("Eve Davis", "eve@company.com", "Engineering", 105000, "2023-08-05"),
]
 
cursor.executemany(
    "INSERT INTO employees (name, email, department, salary, hire_date) VALUES (?, ?, ?, ?, ?)",
    employees
)
 
# Get the last inserted row ID
print(f"Last inserted ID: {cursor.lastrowid}")
 
# Get the number of rows affected
print(f"Rows inserted: {cursor.rowcount}")
 
conn.commit()
conn.close()

INSERT OR REPLACE (Upsert)

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Insert or update if email already exists
cursor.execute("""
    INSERT INTO employees (name, email, department, salary, hire_date)
    VALUES (?, ?, ?, ?, ?)
    ON CONFLICT(email) DO UPDATE SET
        name = excluded.name,
        salary = excluded.salary
""", ("Alice Johnson", "alice@company.com", "Engineering", 100000, "2024-01-15"))
 
conn.commit()
conn.close()

SELECT : lire des données

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Select all rows
cursor.execute("SELECT * FROM employees")
all_rows = cursor.fetchall()
for row in all_rows:
    print(row)  # Returns tuples by default
 
# Select specific columns with a condition
cursor.execute(
    "SELECT name, salary FROM employees WHERE department = ?",
    ("Engineering",)
)
engineers = cursor.fetchall()
for name, salary in engineers:
    print(f"{name}: ${salary:,.0f}")
 
# Select with ordering and limit
cursor.execute("""
    SELECT name, salary FROM employees
    ORDER BY salary DESC
    LIMIT 3
""")
top_earners = cursor.fetchall()
print("Top 3 earners:", top_earners)
 
conn.close()

Obtenir les résultats sous forme de dictionnaires

Par défaut, fetchall() renvoie des tuples. Pour obtenir des dictionnaires (nom de colonne comme clé), utilisez sqlite3.Row :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
conn.row_factory = sqlite3.Row  # Set row factory
 
cursor = conn.cursor()
cursor.execute("SELECT * FROM employees WHERE department = ?", ("Engineering",))
 
for row in cursor.fetchall():
    print(dict(row))  # Convert Row to dict
    print(row["name"], row["salary"])  # Access by column name
 
conn.close()

UPDATE : modifier des données

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Update a single field
cursor.execute(
    "UPDATE employees SET salary = ? WHERE email = ?",
    (110000, "alice@company.com")
)
print(f"Rows updated: {cursor.rowcount}")
 
# Update with a calculation
cursor.execute("""
    UPDATE employees
    SET salary = salary * 1.10
    WHERE department = 'Engineering' AND salary < 100000
""")
print(f"Engineers with raises: {cursor.rowcount}")
 
# Conditional update
cursor.execute("""
    UPDATE employees
    SET is_active = 0
    WHERE hire_date < '2023-01-01'
""")
 
conn.commit()
conn.close()

DELETE : supprimer des données

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Delete specific rows
cursor.execute("DELETE FROM employees WHERE is_active = 0")
print(f"Rows deleted: {cursor.rowcount}")
 
# Delete with a subquery
cursor.execute("""
    DELETE FROM enrollments
    WHERE student_id NOT IN (SELECT id FROM students)
""")
 
# Delete all rows (truncate equivalent)
cursor.execute("DELETE FROM employees")
 
conn.commit()
conn.close()

Requêtes paramétrées et prévention de l’injection SQL

Ne construisez jamais des requêtes SQL via du formatage de chaîne ou des f-strings. Utilisez toujours des requêtes paramétrées pour prévenir les attaques par injection SQL.

Le problème : l’injection SQL

import sqlite3
 
# DANGEROUS: Never do this
user_input = "'; DROP TABLE employees; --"
query = f"SELECT * FROM employees WHERE name = '{user_input}'"
# This would execute: SELECT * FROM employees WHERE name = ''; DROP TABLE employees; --'

La solution : les requêtes paramétrées

SQLite3 prend en charge deux styles de placeholders :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Style 1: Question mark placeholders (positional)
cursor.execute(
    "SELECT * FROM employees WHERE department = ? AND salary > ?",
    ("Engineering", 90000)
)
 
# Style 2: Named placeholders
cursor.execute(
    "SELECT * FROM employees WHERE department = :dept AND salary > :min_salary",
    {"dept": "Engineering", "min_salary": 90000}
)
 
# Named placeholders are clearer with many parameters
cursor.execute("""
    INSERT INTO employees (name, email, department, salary, hire_date)
    VALUES (:name, :email, :dept, :salary, :date)
""", {
    "name": "Frank Green",
    "email": "frank@company.com",
    "dept": "Engineering",
    "salary": 92000,
    "date": "2025-09-01"
})
 
conn.commit()
conn.close()

Les requêtes paramétrées gèrent l’échappement et la mise entre guillemets automatiquement. Le moteur de base de données traite les valeurs des paramètres comme des données, jamais comme du code SQL. Cela élimine l’injection SQL quelle que soit la teneur des entrées.

Clauses WHERE dynamiques

Parfois, vous devez construire des requêtes dynamiquement, par exemple lorsque des filtres sont optionnels. Voici un schéma sûr :

import sqlite3
 
def search_employees(department=None, min_salary=None, is_active=None):
    conn = sqlite3.connect("myapp.db")
    cursor = conn.cursor()
 
    conditions = []
    params = []
 
    if department is not None:
        conditions.append("department = ?")
        params.append(department)
 
    if min_salary is not None:
        conditions.append("salary >= ?")
        params.append(min_salary)
 
    if is_active is not None:
        conditions.append("is_active = ?")
        params.append(is_active)
 
    query = "SELECT * FROM employees"
    if conditions:
        query += " WHERE " + " AND ".join(conditions)
 
    cursor.execute(query, params)
    results = cursor.fetchall()
    conn.close()
    return results
 
# Usage
engineers = search_employees(department="Engineering", min_salary=90000)
active_staff = search_employees(is_active=1)
everyone = search_employees()  # No filters

Travailler avec les transactions

Une transaction regroupe plusieurs opérations SQL en une unité atomique. Soit toutes les opérations réussissent (commit), soit aucune ne prend effet (rollback).

Contrôle manuel des transactions

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
try:
    # Transfer money between accounts
    cursor.execute("UPDATE accounts SET balance = balance - 500 WHERE id = 1")
    cursor.execute("UPDATE accounts SET balance = balance + 500 WHERE id = 2")
 
    # Verify no negative balance
    cursor.execute("SELECT balance FROM accounts WHERE id = 1")
    balance = cursor.fetchone()[0]
    if balance < 0:
        raise ValueError("Insufficient funds")
 
    conn.commit()  # All good, save changes
    print("Transfer complete")
 
except Exception as e:
    conn.rollback()  # Something went wrong, undo everything
    print(f"Transfer failed: {e}")
 
finally:
    conn.close()

Autocommit et niveaux d’isolation

Par défaut, sqlite3 fonctionne en mode « deferred transaction ». Vous pouvez modifier ce comportement :

import sqlite3
 
# Default behavior: transactions are deferred
conn = sqlite3.connect("myapp.db")
 
# Autocommit mode (each statement is its own transaction)
conn = sqlite3.connect("myapp.db", isolation_level=None)
 
# Immediate locking (prevents concurrent write conflicts)
conn = sqlite3.connect("myapp.db")
conn.execute("BEGIN IMMEDIATE")
# ... your operations ...
conn.commit()

Savepoints pour des rollbacks partiels

Les savepoints permettent d’annuler une partie d’une transaction sans tout annuler :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
try:
    cursor.execute("INSERT INTO orders (customer_id, total) VALUES (1, 100)")
 
    # Start a savepoint
    cursor.execute("SAVEPOINT order_items")
 
    try:
        cursor.execute("INSERT INTO order_items (order_id, product, qty) VALUES (1, 'Widget', 5)")
        cursor.execute("INSERT INTO order_items (order_id, product, qty) VALUES (1, 'Gadget', -1)")
        # This might fail due to CHECK constraint
        cursor.execute("RELEASE SAVEPOINT order_items")
    except sqlite3.IntegrityError:
        cursor.execute("ROLLBACK TO SAVEPOINT order_items")
        print("Some items failed, but order was still created")
 
    conn.commit()
except Exception:
    conn.rollback()
finally:
    conn.close()

Récupérer des données : fetchone, fetchall, fetchmany

L’objet curseur fournit trois méthodes pour récupérer les résultats d’une requête.

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
cursor.execute("SELECT * FROM employees ORDER BY salary DESC")
 
# fetchone: Get the next single row (or None if no more rows)
top_earner = cursor.fetchone()
print(f"Top earner: {top_earner}")
 
# fetchmany: Get the next N rows
next_three = cursor.fetchmany(3)
print(f"Next 3: {next_three}")
 
# fetchall: Get all remaining rows
remaining = cursor.fetchall()
print(f"Remaining: {len(remaining)} rows")
 
conn.close()

Itérer efficacement sur les résultats

Pour de grands jeux de résultats, itérez directement sur le curseur au lieu de tout charger en mémoire :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
cursor.execute("SELECT name, salary FROM employees")
 
# Memory-efficient: processes one row at a time
for name, salary in cursor:
    print(f"{name}: ${salary:,.0f}")
 
conn.close()

fetchmany avec traitement par lots

Quand vous traitez des millions de lignes, fetchmany() dans une boucle vous donne le contrôle de l’usage mémoire :

import sqlite3
 
conn = sqlite3.connect("large_data.db")
cursor = conn.cursor()
 
cursor.execute("SELECT * FROM sensor_readings")
 
batch_size = 1000
while True:
    rows = cursor.fetchmany(batch_size)
    if not rows:
        break
    # Process batch
    for row in rows:
        pass  # your processing logic
 
conn.close()

Requêtes avancées

JOINs

import sqlite3
 
conn = sqlite3.connect("school.db")
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
 
# INNER JOIN: Students with their enrolled courses
cursor.execute("""
    SELECT s.name AS student, c.title AS course, e.grade
    FROM enrollments e
    INNER JOIN students s ON e.student_id = s.id
    INNER JOIN courses c ON e.course_id = c.id
    ORDER BY s.name, c.title
""")
 
for row in cursor.fetchall():
    print(f"{row['student']} - {row['course']}: {row['grade'] or 'In Progress'}")
 
# LEFT JOIN: All students, even those not enrolled
cursor.execute("""
    SELECT s.name, COUNT(e.id) AS course_count
    FROM students s
    LEFT JOIN enrollments e ON s.id = e.student_id
    GROUP BY s.id
    ORDER BY course_count DESC
""")
 
for row in cursor.fetchall():
    print(f"{row['name']}: {row['course_count']} courses")
 
conn.close()

Agrégation

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Department statistics
cursor.execute("""
    SELECT
        department,
        COUNT(*) AS headcount,
        ROUND(AVG(salary), 2) AS avg_salary,
        MIN(salary) AS min_salary,
        MAX(salary) AS max_salary,
        SUM(salary) AS total_payroll
    FROM employees
    WHERE is_active = 1
    GROUP BY department
    HAVING COUNT(*) >= 2
    ORDER BY avg_salary DESC
""")
 
print(f"{'Department':<15} {'Count':<8} {'Avg Salary':<12} {'Min':<10} {'Max':<10}")
print("-" * 55)
for row in cursor.fetchall():
    dept, count, avg_sal, min_sal, max_sal, total = row
    print(f"{dept:<15} {count:<8} ${avg_sal:<11,.0f} ${min_sal:<9,.0f} ${max_sal:<9,.0f}")
 
conn.close()

Sous-requêtes

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Employees earning above their department average
cursor.execute("""
    SELECT name, department, salary
    FROM employees e
    WHERE salary > (
        SELECT AVG(salary) FROM employees
        WHERE department = e.department
    )
    ORDER BY department, salary DESC
""")
 
for name, dept, salary in cursor.fetchall():
    print(f"{name} ({dept}): ${salary:,.0f}")
 
# Most recent hire in each department
cursor.execute("""
    SELECT name, department, hire_date
    FROM employees
    WHERE (department, hire_date) IN (
        SELECT department, MAX(hire_date)
        FROM employees
        GROUP BY department
    )
""")
 
for row in cursor.fetchall():
    print(row)
 
conn.close()

Fonctions de fenêtre (SQLite 3.25+)

SQLite prend en charge les fonctions de fenêtre pour les cumuls, les classements et d’autres requêtes analytiques :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
cursor = conn.cursor()
 
# Rank employees by salary within each department
cursor.execute("""
    SELECT
        name,
        department,
        salary,
        RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
        RANK() OVER (ORDER BY salary DESC) AS overall_rank
    FROM employees
    WHERE is_active = 1
""")
 
for name, dept, salary, dept_rank, overall_rank in cursor.fetchall():
    print(f"#{overall_rank} overall, #{dept_rank} in {dept}: {name} (${salary:,.0f})")
 
# Running total of payroll by hire date
cursor.execute("""
    SELECT
        name,
        hire_date,
        salary,
        SUM(salary) OVER (ORDER BY hire_date) AS running_total
    FROM employees
    ORDER BY hire_date
""")
 
for name, date, salary, running in cursor.fetchall():
    print(f"{date} | {name}: ${salary:,.0f} | Running total: ${running:,.0f}")
 
conn.close()

SQLite avec pandas

pandas a un support SQLite intégré via read_sql() et to_sql(). Cela facilite le transfert de données entre des DataFrames et des bases SQLite.

Lire des résultats SQL dans un DataFrame

import sqlite3
import pandas as pd
 
conn = sqlite3.connect("myapp.db")
 
# Read a SQL query into a DataFrame
df = pd.read_sql("SELECT * FROM employees WHERE is_active = 1", conn)
print(df.head())
print(df.describe())
 
# Parameterized query with pandas
df = pd.read_sql(
    "SELECT * FROM employees WHERE department = ? AND salary > ?",
    conn,
    params=("Engineering", 90000)
)
 
# Read an entire table
df = pd.read_sql("SELECT * FROM employees", conn, index_col="id")
 
conn.close()

Écrire un DataFrame dans SQLite

import sqlite3
import pandas as pd
 
# Create a sample DataFrame
df = pd.DataFrame({
    "product": ["Laptop", "Phone", "Tablet", "Monitor", "Keyboard"],
    "price": [999.99, 699.99, 449.99, 349.99, 79.99],
    "stock": [50, 200, 75, 30, 500],
    "category": ["Electronics", "Electronics", "Electronics", "Peripherals", "Peripherals"]
})
 
conn = sqlite3.connect("store.db")
 
# Write DataFrame to a new table
df.to_sql("products", conn, if_exists="replace", index=False)
 
# Append to an existing table
new_products = pd.DataFrame({
    "product": ["Mouse", "Webcam"],
    "price": [49.99, 89.99],
    "stock": [300, 100],
    "category": ["Peripherals", "Peripherals"]
})
new_products.to_sql("products", conn, if_exists="append", index=False)
 
# Verify
result = pd.read_sql("SELECT * FROM products", conn)
print(result)
 
conn.close()

Workflow aller-retour : CSV → SQLite → analyse

import sqlite3
import pandas as pd
 
# Step 1: Load CSV into SQLite
df = pd.read_csv("sales_data.csv")
conn = sqlite3.connect("analytics.db")
df.to_sql("sales", conn, if_exists="replace", index=False)
 
# Step 2: Run SQL analytics
summary = pd.read_sql("""
    SELECT
        region,
        strftime('%Y-%m', sale_date) AS month,
        COUNT(*) AS transactions,
        SUM(amount) AS revenue,
        AVG(amount) AS avg_transaction
    FROM sales
    GROUP BY region, month
    ORDER BY month, revenue DESC
""", conn)
 
print(summary)
 
# Step 3: Export results
summary.to_csv("monthly_summary.csv", index=False)
 
conn.close()

Une fois vos résultats de requêtes dans un DataFrame, vous pouvez les visualiser directement avec PyGWalker (opens in a new tab). PyGWalker transforme n’importe quel pandas DataFrame en une interface de visualisation interactive similaire à Tableau, vous permettant de glisser-déposer des champs pour créer des graphiques sans écrire de code de plotting :

import pygwalker as pyg
 
# Turn your SQL query results into an interactive visualization
walker = pyg.walk(summary)

Pour un workflow encore plus fluide, RunCell (opens in a new tab) propose un environnement Jupyter propulsé par l’IA où vous pouvez écrire des requêtes SQL, bénéficier d’un debug assisté par IA et visualiser instantanément les résultats — le tout dans un seul notebook.

Gestion des erreurs

Le module sqlite3 définit plusieurs types d’exceptions. Capturer des exceptions spécifiques conduit à de meilleurs messages d’erreur et à de meilleures stratégies de récupération.

import sqlite3
 
def safe_insert(db_path, name, email, salary):
    """Insert an employee with proper error handling."""
    try:
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute(
            "INSERT INTO employees (name, email, salary, hire_date) VALUES (?, ?, ?, date('now'))",
            (name, email, salary)
        )
        conn.commit()
        return cursor.lastrowid
 
    except sqlite3.IntegrityError as e:
        # Unique constraint violated (duplicate email, NOT NULL violated, etc.)
        print(f"Data integrity error: {e}")
        return None
 
    except sqlite3.OperationalError as e:
        # Table doesn't exist, SQL syntax error, database locked, etc.
        print(f"Operational error: {e}")
        return None
 
    except sqlite3.DatabaseError as e:
        # Corrupted database, disk full, etc.
        print(f"Database error: {e}")
        return None
 
    except sqlite3.ProgrammingError as e:
        # Using a closed cursor, wrong number of parameters, etc.
        print(f"Programming error: {e}")
        return None
 
    finally:
        if conn:
            conn.close()

Hiérarchie des exceptions SQLite3

ExceptionParentCauses courantes
sqlite3.WarningExceptionProblèmes non fatals
sqlite3.ErrorExceptionClasse de base de toutes les erreurs sqlite3
sqlite3.InterfaceErrorErrorMauvaise utilisation de l’interface DB-API
sqlite3.DatabaseErrorErrorErreurs liées à la base
sqlite3.DataErrorDatabaseErrorProblèmes de traitement des données
sqlite3.OperationalErrorDatabaseErrorBase verrouillée, table manquante, erreur SQL
sqlite3.IntegrityErrorDatabaseErrorViolations UNIQUE, NOT NULL, FOREIGN KEY
sqlite3.InternalErrorDatabaseErrorErreur interne du module sqlite3
sqlite3.ProgrammingErrorDatabaseErrorUtilisation incorrecte de l’API
sqlite3.NotSupportedErrorDatabaseErrorFonctionnalité non supportée

Conseils de performance

Activer le mode WAL

Le mode Write-Ahead Logging (WAL) permet des lectures concurrentes pendant l’écriture et améliore les performances d’écriture :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
conn.execute("PRAGMA journal_mode=WAL")
# Returns 'wal' if successful

Le mode WAL est particulièrement utile lorsqu’un lecteur (par ex. un serveur web) et un écrivain (par ex. une tâche de fond) accèdent à la même base.

Utiliser executemany pour les insertions en masse

executemany est nettement plus rapide que d’appeler execute dans une boucle, car cela réduit les allers-retours Python ↔ SQLite :

import sqlite3
import time
 
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE numbers (value INTEGER)")
 
data = [(i,) for i in range(100_000)]
 
# Slow: individual inserts
start = time.perf_counter()
for d in data:
    conn.execute("INSERT INTO numbers VALUES (?)", d)
conn.commit()
slow_time = time.perf_counter() - start
 
conn.execute("DELETE FROM numbers")
 
# Fast: executemany
start = time.perf_counter()
conn.executemany("INSERT INTO numbers VALUES (?)", data)
conn.commit()
fast_time = time.perf_counter() - start
 
print(f"Individual inserts: {slow_time:.2f}s")
print(f"executemany:        {fast_time:.2f}s")
print(f"Speedup:            {slow_time / fast_time:.1f}x")
 
conn.close()

Créer des index pour les colonnes fréquemment interrogées

Les index accélèrent fortement les requêtes SELECT au prix d’écritures un peu plus lentes et de plus d’espace disque :

import sqlite3
 
conn = sqlite3.connect("myapp.db")
 
# Single column index
conn.execute("CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department)")
 
# Composite index (for queries that filter on both columns)
conn.execute("CREATE INDEX IF NOT EXISTS idx_employees_dept_salary ON employees(department, salary)")
 
# Unique index (also enforces uniqueness)
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_employees_email ON employees(email)")
 
# Check existing indexes
cursor = conn.execute("SELECT name, sql FROM sqlite_master WHERE type='index'")
for name, sql in cursor.fetchall():
    print(f"{name}: {sql}")
 
conn.close()

Utiliser EXPLAIN QUERY PLAN pour vérifier les performances

import sqlite3
 
conn = sqlite3.connect("myapp.db")
 
# Check how SQLite executes a query
result = conn.execute("""
    EXPLAIN QUERY PLAN
    SELECT * FROM employees WHERE department = 'Engineering' AND salary > 90000
""").fetchall()
 
for row in result:
    print(row)
# Output shows whether an index is used or a full table scan occurs
 
conn.close()

PRAGMAs supplémentaires pour la performance

import sqlite3
 
conn = sqlite3.connect("myapp.db")
 
# Increase cache size (default is 2000 pages = ~8MB)
conn.execute("PRAGMA cache_size = -64000")  # 64MB (negative = KB)
 
# Reduce disk syncs for speed (less durable but much faster)
conn.execute("PRAGMA synchronous = NORMAL")  # Options: OFF, NORMAL, FULL (default)
 
# Store temp tables in memory
conn.execute("PRAGMA temp_store = MEMORY")
 
# Check database integrity
result = conn.execute("PRAGMA integrity_check").fetchone()
print(f"Integrity: {result[0]}")  # Should print 'ok'
 
conn.close()

Exemple concret : CLI de gestion de tâches

Voici une application complète de gestion de tâches qui illustre tous les concepts couverts dans ce guide. Elle utilise SQLite pour le stockage persistant et gère les priorités, les tags et les dates d’échéance.

import sqlite3
import sys
from datetime import datetime, date
from contextlib import contextmanager
 
DB_PATH = "tasks.db"
 
@contextmanager
def get_db():
    """Get a database connection with Row factory and foreign keys enabled."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    conn.execute("PRAGMA journal_mode = WAL")
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()
 
def init_db():
    """Create tables if they don't exist."""
    with get_db() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS tasks (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                description TEXT DEFAULT '',
                priority INTEGER DEFAULT 2 CHECK(priority BETWEEN 1 AND 4),
                status TEXT DEFAULT 'todo' CHECK(status IN ('todo', 'in_progress', 'done')),
                due_date TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                completed_at TEXT
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS tags (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT UNIQUE NOT NULL
            )
        """)
        conn.execute("""
            CREATE TABLE IF NOT EXISTS task_tags (
                task_id INTEGER NOT NULL,
                tag_id INTEGER NOT NULL,
                PRIMARY KEY (task_id, tag_id),
                FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
                FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
            )
        """)
        conn.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority)")
        conn.execute("CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date)")
 
def add_task(title, description="", priority=2, due_date=None, tags=None):
    """Add a new task with optional tags."""
    with get_db() as conn:
        cursor = conn.execute(
            "INSERT INTO tasks (title, description, priority, due_date) VALUES (?, ?, ?, ?)",
            (title, description, priority, due_date)
        )
        task_id = cursor.lastrowid
 
        if tags:
            for tag_name in tags:
                conn.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag_name,))
                tag_id = conn.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)).fetchone()["id"]
                conn.execute("INSERT INTO task_tags (task_id, tag_id) VALUES (?, ?)", (task_id, tag_id))
 
        print(f"Task #{task_id} created: {title}")
        return task_id
 
def list_tasks(status=None, priority=None, tag=None):
    """List tasks with optional filters."""
    with get_db() as conn:
        conditions = []
        params = []
 
        if status:
            conditions.append("t.status = ?")
            params.append(status)
        if priority:
            conditions.append("t.priority = ?")
            params.append(priority)
        if tag:
            conditions.append("t.id IN (SELECT task_id FROM task_tags tt JOIN tags tg ON tt.tag_id = tg.id WHERE tg.name = ?)")
            params.append(tag)
 
        query = """
            SELECT t.id, t.title, t.priority, t.status, t.due_date,
                   GROUP_CONCAT(tg.name, ', ') AS tags
            FROM tasks t
            LEFT JOIN task_tags tt ON t.id = tt.task_id
            LEFT JOIN tags tg ON tt.tag_id = tg.id
        """
        if conditions:
            query += " WHERE " + " AND ".join(conditions)
        query += " GROUP BY t.id ORDER BY t.priority ASC, t.due_date ASC"
 
        rows = conn.execute(query, params).fetchall()
 
        priority_labels = {1: "URGENT", 2: "HIGH", 3: "MEDIUM", 4: "LOW"}
        print(f"\n{'ID':<5} {'Priority':<10} {'Status':<13} {'Due':<12} {'Title':<30} {'Tags'}")
        print("-" * 85)
        for row in rows:
            due = row["due_date"] or "—"
            tags_str = row["tags"] or "—"
            p_label = priority_labels.get(row["priority"], "?")
            print(f"{row['id']:<5} {p_label:<10} {row['status']:<13} {due:<12} {row['title']:<30} {tags_str}")
 
        print(f"\nTotal: {len(rows)} tasks")
 
def complete_task(task_id):
    """Mark a task as done."""
    with get_db() as conn:
        result = conn.execute(
            "UPDATE tasks SET status = 'done', completed_at = ? WHERE id = ?",
            (datetime.now().isoformat(), task_id)
        )
        if result.rowcount:
            print(f"Task #{task_id} marked as done.")
        else:
            print(f"Task #{task_id} not found.")
 
def stats():
    """Show task statistics."""
    with get_db() as conn:
        row = conn.execute("""
            SELECT
                COUNT(*) AS total,
                SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END) AS todo,
                SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress,
                SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done,
                SUM(CASE WHEN due_date < date('now') AND status != 'done' THEN 1 ELSE 0 END) AS overdue
            FROM tasks
        """).fetchone()
 
        print(f"\nTask Statistics:")
        print(f"  Total:       {row['total']}")
        print(f"  To Do:       {row['todo']}")
        print(f"  In Progress: {row['in_progress']}")
        print(f"  Done:        {row['done']}")
        print(f"  Overdue:     {row['overdue']}")
 
# Initialize and demo
if __name__ == "__main__":
    init_db()
 
    # Add sample tasks
    add_task("Design database schema", priority=1, due_date="2026-02-20", tags=["backend", "database"])
    add_task("Write API endpoints", priority=2, due_date="2026-02-25", tags=["backend", "api"])
    add_task("Create unit tests", priority=3, tags=["testing"])
    add_task("Update documentation", priority=4, due_date="2026-03-01", tags=["docs"])
 
    # List and stats
    list_tasks()
    stats()
    complete_task(1)
    list_tasks(status="done")

SQLite vs PostgreSQL vs MySQL vs MongoDB

FonctionnalitéSQLitePostgreSQLMySQLMongoDB
TypeEmbeddedClient-serverClient-serverDocument store
Mise en placeZéro configInstaller + configurerInstaller + configurerInstaller + configurer
StockageFichier uniqueRépertoire serveurRépertoire serveurRépertoire serveur
Taille max DB281 TBUnlimited256 TBUnlimited
Écritures concurrentesLimité (verrou de fichier)Excellent (MVCC)Bon (verrous de ligne)Bon (verrous de document)
Support SQLLa plupart de SQL92SQL complet + extensionsSQL completMQL (pas SQL)
Types de données5 classes de stockage40+ types30+ typesTypes BSON
Recherche plein texteExtension FTS5tsvector intégréFULLTEXT intégréIndex texte intégré
Support JSONExtension JSON1JSONB natifJSON natifNatif (c’est du JSON)
RéplicationPas intégréStreaming + logiquePrimary-replicaReplica sets
Idéal pourEmbarqué, local, prototypesApps web, analyticsApps web, CMSSchéma flexible, documents
Lib Pythonsqlite3 (built-in)psycopg2mysql-connectorpymongo
Hébergement requisNonOuiOuiOui
Coût de départGratuit, zéro overheadGratuit, mais serveur requisGratuit, mais serveur requisGratuit, mais serveur requis

Le choix est simple : utilisez SQLite lorsque votre application est le seul processus qui écrit dans la base. Utilisez PostgreSQL ou MySQL lorsque vous avez besoin d’écritures multi-utilisateurs concurrentes, de réplication ou d’accès réseau. Utilisez MongoDB lorsque vos données sont orientées documents et s’adaptent mal à un schéma relationnel.

Foire aux questions (FAQ)

SQLite est-il adapté à une utilisation en production ?

Oui. SQLite est utilisé en production par Android, iOS, tous les navigateurs web majeurs, les logiciels avioniques d’Airbus, et d’innombrables applications desktop. Il gère jusqu’à 281 téraoctets et des millions de lignes. Il n’est pas adapté aux applications web avec de nombreuses écritures concurrentes et beaucoup d’utilisateurs simultanés, mais pour la plupart des autres scénarios, il fonctionne très bien.

Comment vérifier quelle version de SQLite Python utilise ?

Utilisez sqlite3.sqlite_version pour vérifier la version de la bibliothèque SQLite et sqlite3.version pour la version du module Python. Par exemple : import sqlite3; print(sqlite3.sqlite_version) peut afficher 3.45.1.

Plusieurs processus peuvent-ils lire la même base SQLite ?

Oui. Plusieurs processus peuvent lire simultanément la même base SQLite. Les opérations d’écriture acquièrent un verrou exclusif sur le fichier de base. Avec le mode WAL activé, les lecteurs ne bloquent pas les écrivains et les écrivains ne bloquent pas les lecteurs. Un seul écrivain peut opérer à la fois.

Comment sauvegarder une base SQLite en toute sécurité ?

Utilisez l’API de sauvegarde sqlite3 : source.backup(dest). Vous pouvez aussi copier le fichier de base en toute sécurité lorsqu’aucune écriture n’est en cours. Ne copiez jamais le fichier pendant qu’une transaction d’écriture est active, car cela peut corrompre la sauvegarde.

import sqlite3
 
source = sqlite3.connect("myapp.db")
backup = sqlite3.connect("myapp_backup.db")
source.backup(backup)
backup.close()
source.close()

Comment stocker les dates et heures dans SQLite ?

SQLite n’a pas de type DATE ou DATETIME natif. Stockez les dates en TEXT au format ISO 8601 ('2026-02-18' ou '2026-02-18T14:30:00'). Les fonctions de dates intégrées de SQLite (date(), time(), datetime(), strftime()) fonctionnent avec ce format. Sinon, stockez des timestamps Unix sous forme d’INTEGER.

SQLite prend-il en charge le chiffrement ?

SQLite open-source standard n’inclut pas le chiffrement. SQLite Encryption Extension (SEE) est un produit commercial payant. Des alternatives gratuites incluent SQLCipher (open source) et pysqlcipher3 pour l’intégration Python.

Comment prévenir l’injection SQL en Python avec sqlite3 ?

Utilisez toujours des requêtes paramétrées avec des placeholders ? ou :name. Ne construisez jamais des chaînes SQL via f-strings, .format() ou le formatage % avec des entrées utilisateur. Le module sqlite3 gère automatiquement l’échappement lorsque vous passez les paramètres séparément.

Conclusion

Le module sqlite3 de Python vous offre une base de données relationnelle complète, sans installation. Pour les applications locales, les prototypes, les scripts d’analyse de données et les tests, SQLite est difficile à battre. Les schémas clés à retenir : utilisez toujours des requêtes paramétrées pour éviter l’injection SQL, utilisez des context managers pour une gestion propre des connexions, activez le mode WAL pour une meilleure concurrence et utilisez executemany pour les opérations en masse.

Pour les workflows d’analyse, la combinaison SQLite + pandas est très puissante : chargez les données avec to_sql(), interrogez-les avec read_sql(), et visualisez les résultats avec PyGWalker (opens in a new tab). Quand votre projet dépasse les limites de concurrence de SQLite, passer à PostgreSQL requiert généralement peu de changements de code, car l’interface DB-API 2.0 est cohérente d’un driver de base de données à l’autre.

📚