NSI · Terminale

Bases de données : interroger une base SQLite avec Python

Découvrez comment manipuler et interroger une base de données relationnelle SQLite en Python avec le module standard sqlite3 (connexion, curseurs, requêtes paramétrées, transactions, prévention des injections SQL et annales du Bac NSI).

Interroger une base SQLite avec Python

Introduction

Dans les chapitres précédents, nous avons appris à concevoir un modèle relationnel et à écrire des requêtes SQL pour interroger une base de données.

Dans une application réelle (site web, application mobile, logiciel de gestion, analyse de données), les requêtes SQL ne sont pas exécutées manuellement par un humain dans une console, mais de manière automatisée par un programme informatique.

Python dispose nativement dans sa bibliothèque standard d'un module dédié : `sqlite3`. Aucun outil externe ni installation supplémentaire (pip) n'est nécessaire.

Schéma de fonctionnement

Le programme Python communique avec la base de données via le module sqlite3 :

  1. Programme Python (`script.py`) : Prépare et transmet la requête SQL au moteur SQLite à l'aide de l'objet curseur (curseur.execute(...)).
  2. Moteur SQLite (`sqlite3`) : Analyse la requête, effectue les opérations de lecture ou d'écriture dans le fichier de base de données (bibliotheque.db).
  3. Retour des données : Le moteur SQLite renvoie les enregistrements sous forme de structures de données Python (curseur.fetchall() renvoie une liste de tuples).

Le cycle de communication avec une base de données

Toute interaction entre Python et SQLite suit systématiquement 5 étapes fondamentales :

  1. Importer le module sqlite3.
  2. Établir une connexion vers le fichier de base de données (sqlite3.connect).
  3. Créer un curseur pour exécuter les instructions SQL (connexion.cursor()).
  4. Exécuter les requêtes SQL (curseur.execute()) et récupérer les résultats (fetchall(), fetchone()).
  5. Valider les modifications (connexion.commit()) et fermer la connexion (connexion.close()).

1. Établir la connexion à la base de données

La fonction sqlite3.connect(nom_fichier) ouvre la connexion avec la base de données. Si le fichier n'existe pas encore sur votre disque, SQLite le crée automatiquement.

Python
1
2
3
4
5
6
7
import sqlite3
# Connexion à la base de données (fichier local)
connexion = sqlite3.connect("bibliotheque.db")
# Toujours fermer la connexion lorsque le traitement est terminé
connexion.close()
À retenir / Remarque

Base de données en mémoire vive

Pour des tests unitaires ou des exercices rapides sans créer de fichier sur le disque dur, vous pouvez utiliser la base spéciale en mémoire :

Python
1
connexion = sqlite3.connect(":memory:")

2. Le rôle fondamental du Curseur

Le curseur est l'objet intermédiaire qui transmet vos commandes SQL au SGBD et stocke temporairement les résultats renvoyés par la base.

Python
1
2
3
4
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
curseur = connexion.cursor() # Création du curseur

Le curseur fournit les méthodes suivantes :

  • curseur.execute(requete_sql, [parametres]) : exécute une seule requête SQL.
  • curseur.executemany(requete_sql, liste_de_tuples) : exécute la même requête avec plusieurs jeux de valeurs.
  • curseur.fetchall() : renvoie toutes les lignes du résultat sous forme d'une liste de tuples.
  • curseur.fetchone() : renvoie la première ligne (un tuple) ou None si aucun résultat.

3. Créer une table et insérer des données

A. Création de table (DDL)

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
curseur = connexion.cursor()
# Création de la table LIVRES si elle n'existe pas déjà
requete_creation = """
CREATE TABLE IF NOT EXISTS LIVRES (
id INTEGER PRIMARY KEY,
titre TEXT NOT NULL,
auteur TEXT NOT NULL,
ann_publi INTEGER,
note INTEGER
);
"""
curseur.execute(requete_creation)
connexion.commit() # Validation de la transaction
connexion.close()

B. Insertion de données & Rôle crucial de commit()

Lorsqu'on effectue une opération d'écriture (INSERT, UPDATE, DELETE), les modifications restent temporaires dans une transaction. Pour les enregistrer définitivement dans le fichier, il faut appeler connexion.commit().

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
curseur = connexion.cursor()
# 1. Insertion d'un livre unique
curseur.execute("""
INSERT INTO LIVRES (id, titre, auteur, ann_publi, note)
VALUES (1, '1984', 'Orwell', 1949, 10);
""")
# 2. Insertion multiple avec executemany
livres_a_inserer = [
(2, "Dune", "Herbert", 1965, 8),
(3, "Fondation", "Asimov", 1951, 9),
(4, "Fahrenheit 451", "Bradbury", 1953, 7),
(5, "Blade Runner", "K.Dick", 1968, 8),
(6, "Les Robots", "Asimov", 1950, 9)
]
curseur.executemany("""
INSERT INTO LIVRES (id, titre, auteur, ann_publi, note)
VALUES (?, ?, ?, ?, ?);
""", livres_a_inserer)
# VALIDATION INDISPENSABLE
connexion.commit()
connexion.close()
Attention

Attention : Piège classique au Bac NSI

Si vous oubliez d'appeler connexion.commit(), les données insérées ou modifiées seront perdues dès la fermeture du script (Rollback automatique) !

4. Interroger la base et récupérer les enregistrements

A. Récupérer tous les résultats avec fetchall()

La méthode curseur.fetchall() renvoie une liste de tuples. Chaque tuple correspond à un enregistrement (ligne) de la table.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
curseur = connexion.cursor()
curseur.execute("SELECT titre, auteur, note FROM LIVRES WHERE note >= 8 ORDER BY note DESC;")
resultats = curseur.fetchall()
print("Type de retour :", type(resultats)) # <class 'list'>
print("Contenu :", resultats)
# Parcours des résultats
for livre in resultats:
titre, auteur, note = livre
print(f"- {titre} par {auteur} (Note : {note}/10)")
connexion.close()

Sortie console :

Console / Sortie
1
2
3
4
5
6
7
Type de retour : <class 'list'>
Contenu : [('1984', 'Orwell', 10), ('Fondation', 'Asimov', 9), ('Les Robots', 'Asimov', 9), ('Dune', 'Herbert', 8), ('Blade Runner', 'K.Dick', 8)]
- 1984 par Orwell (Note : 10/10)
- Fondation par Asimov (Note : 9/10)
- Les Robots par Asimov (Note : 9/10)
- Dune par Herbert (Note : 8/10)
- Blade Runner par K.Dick (Note : 8/10)

B. Récupérer une ligne unique avec fetchone()

Lorsque la requête ne doit renvoyer qu'un seul résultat (par exemple une recherche par clé primaire ou une fonction d'agrégation COUNT), on utilise curseur.fetchone().

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
curseur = connexion.cursor()
# Recherche par identifiant unique
curseur.execute("SELECT titre, auteur FROM LIVRES WHERE id = 1;")
livre = curseur.fetchone()
if livre is not None:
print(f"Livre trouvé : {livre[0]} par {livre[1]}")
else:
print("Aucun livre ne correspond à cet identifiant.")
# Comptage total des livres
curseur.execute("SELECT COUNT(*) FROM LIVRES;")
total = curseur.fetchone()[0]
print(f"Nombre total de livres enregistrés : {total}")
connexion.close()

C. Parcours itératif direct du curseur (Économie de mémoire)

Pour de très grandes bases de données contenant des millions de lignes, charger toute la table en mémoire avec fetchall() peut saturer la RAM. On peut itérer directement sur l'objet curseur :

Python
1
2
3
curseur.execute("SELECT titre, ann_publi FROM LIVRES;")
for ligne in curseur:
print(ligne[0], "-", ligne[1])

5. Requêtes paramétrées et Sécurité (Injections SQL)

C'est l'un des points les plus importants au programme du Bac NSI.

Danger : la concaténation de chaînes (f-strings)

Supposons qu'un utilisateur saisisse le nom d'un auteur via un formulaire web ou un input() :

Python
1
2
3
4
5
6
# Code très vulnérable et dangereux ! Ne jamais faire cela :
auteur_recherche = input("Nom de l'auteur : ")
# Concaténation non sécurisée
requete = f"SELECT * FROM LIVRES WHERE auteur = '{auteur_recherche}';"
curseur.execute(requete)

Si un pirate saisit comme nom d'auteur :

Console / Sortie
1
' OR '1'='1

La requête exécutée devient :

SQL
1
SELECT * FROM LIVRES WHERE auteur = '' OR '1'='1';

Comme la condition '1'='1' est toujours vraie, la base de données renverra l'intégralité des données, contournant toute authentification ou sécurité. Un pirate pourrait même injecter '; DROP TABLE LIVRES; -- pour détruire la table !

Bonne pratique : les requêtes préparées paramétrées avec ?

Pour se protéger contre les injections SQL, on utilise le symbole réservé `?` (placeholder) et on transmet les valeurs dans un tuple en second paramètre de execute() :

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
curseur = connexion.cursor()
auteur_recherche = "Asimov"
# Le tuple (auteur_recherche,) contient les arguments
curseur.execute("SELECT titre, ann_publi FROM LIVRES WHERE auteur = ?;", (auteur_recherche,))
for titre, annee in curseur.fetchall():
print(f"- {titre} ({annee})")
connexion.close()
À retenir / Remarque

Le driver SQLite se charge d'échapper correctement les caractères spéciaux et d'associer le bon type de données, rendant toute injection SQL impossible.

6. Accès par nom de colonne avec sqlite3.Row

Par défaut, SQLite renvoie les lignes sous forme de simples tuples indexés par des entiers (ligne[0], ligne[1]). Pour rendre le code beaucoup plus lisible, on peut configurer connexion.row_factory :

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sqlite3
connexion = sqlite3.connect("bibliotheque.db")
# Configuration pour accéder aux colonnes par leur nom
connexion.row_factory = sqlite3.Row
curseur = connexion.cursor()
curseur.execute("SELECT id, titre, auteur, note FROM LIVRES WHERE note >= 9;")
for row in curseur.fetchall():
# Accès intuitif comme avec un dictionnaire :
print(f"[{row['id']}] {row['titre']} - Auteur: {row['auteur']} (Note: {row['note']})")
connexion.close()

7. Exemple complet d'architecture modulaire

Voici un module Python complet et professionnel démontrant les bonnes pratiques de programmation en Terminale NSI (fonctions pures, gestion des transactions, requêtes paramétrées) :

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import sqlite3
def initialiser_base(nom_fichier="bibliotheque.db"):
"""Crée la structure de la table si elle n'existe pas."""
with sqlite3.connect(nom_fichier) as con:
cur = con.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS LIVRES (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titre TEXT NOT NULL,
auteur TEXT NOT NULL,
ann_publi INTEGER,
note INTEGER CHECK(note BETWEEN 0 AND 10)
);
""")
con.commit()
def ajouter_livre(titre: str, auteur: str, annee: int, note: int, nom_fichier="bibliotheque.db") -> int:
"""Insère un nouveau livre et renvoie son identifiant unique généré."""
with sqlite3.connect(nom_fichier) as con:
cur = con.cursor()
cur.execute("""
INSERT INTO LIVRES (titre, auteur, ann_publi, note)
VALUES (?, ?, ?, ?);
""", (titre, auteur, annee, note))
con.commit()
return cur.lastrowid
def chercher_par_auteur(auteur: str, nom_fichier="bibliotheque.db") -> list:
"""Renvoie la liste des titres publiés par un auteur donné."""
with sqlite3.connect(nom_fichier) as con:
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute("""
SELECT titre, ann_publi, note
FROM LIVRES
WHERE auteur = ?
ORDER BY ann_publi ASC;
""", (auteur,))
return [dict(row) for row in cur.fetchall()]
def modifier_note(id_livre: int, nouvelle_note: int, nom_fichier="bibliotheque.db") -> bool:
"""Met à jour la note d'un livre identifié par son id."""
with sqlite3.connect(nom_fichier) as con:
cur = con.cursor()
cur.execute("""
UPDATE LIVRES
SET note = ?
WHERE id = ?;
""", (nouvelle_note, id_livre))
con.commit()
return cur.rowcount > 0
# Programme principal de test
if __name__ == "__main__":
initialiser_base()
id_cree = ajouter_livre("Le Cycle de Fondation", "Asimov", 1951, 10)
print(f"Livre inséré avec l'ID #{id_cree}")
livres_asimov = chercher_par_auteur("Asimov")
print("Livres d'Asimov :", livres_asimov)

Fiche de Révision Synthétique (Bac NSI)

Méthode / ConceptRôle & Syntaxe
sqlite3.connect(fichier)Ouvre ou crée la base de données.
con.cursor()Crée le curseur d'exécution des requêtes.
cur.execute(sql, tuple_params)Exécute une requête SQL de manière sécurisée.
cur.executemany(sql, liste_tuples)Exécute une insertion ou mise à jour par lot.
cur.fetchall()Récupère tous les enregistrements (liste de tuples).
cur.fetchone()Récupère le premier enregistrement ou None.
con.commit()Valide et enregistre de façon permanente les modifications.
con.close()Ferme la connexion et libère les verrous de fichier.
? (Placeholder)Empêche formellement les failles de sécurité par injection SQL.

Exercices du Baccalauréat à réviser

Les interactions entre bases de données relationnelles et scripts Python font l'objet d'exercices réguliers au Bac NSI :

  • Sujet 2024 - Groupe 2 (Jour 2) - Exercice 3 : Conception de requêtes SQL et manipulation de résultats en Python (sqlite3, parcours de tuples et calculs de moyennes).
  • Sujet 2023 - Métropole (Jour 1) - Exercice 3 : Schéma relationnel, cardinalités, requêtes de mise à jour et écriture de fonctions Python sécurisées avec requêtes paramétrées.
  • Sujet 2022 - Centres Étrangers - Exercice 4 : Analyse de vulnérabilités d'injection SQL et conversion de requêtes concaténées vers des requêtes préparées.

Avez-vous compris et assimilé ce cours ?

+50 XP

Étudiez le contenu activement pendant au moins 45 secondes pour pouvoir débloquer la validation au mérite.

Temps d'étude actif :0s / 45s