Chapitre 09 — Modules et bibliothèque standard¶
Durée estimée : 4 à 5 heures (cours + exercices + mini-projet)
Prérequis : Chapitre 08 — Erreurs et débogage (et en particulier les fonctions du chapitre 05).
Objectifs — À la fin de ce chapitre, tu sauras : - importer un module de plusieurs façons (
import,from … import,as) et choisir la bonne ; - découper ton propre programme en plusieurs fichiers (modules) et en dossiers (paquets) ; - comprendre et utiliserif __name__ == "__main__":; - utiliser les modules incontournables de la bibliothèque standard :random,math,datetime,os,sys,collections; - explorer un module inconnu avecdir(),help()et la documentation officielle ; - éviter les pièges classiques (fichier nommérandom.py, import circulaire…).
Cours¶
1. Pourquoi des modules ?¶
Jusqu'ici, tous tes programmes tenaient dans un seul fichier. C'est parfait pour 50 lignes. Mais un vrai programme en fait vite 500, 5 000 ou 50 000. Tout mettre dans un seul fichier, ce serait comme ranger toutes tes affaires dans un unique tiroir géant : on ne retrouve plus rien.
Un module, c'est simplement un fichier .py qui contient des fonctions, des variables (et plus tard des classes) qu'on peut réutiliser ailleurs. Les modules servent à :
- organiser : un fichier par thème (
calculs.py,affichage.py,sauvegarde.py) ; - réutiliser : écrire une fonction une fois, l'utiliser dans dix programmes ;
- profiter du travail des autres : Python est livré avec des centaines de modules prêts à l'emploi.
Cette collection de modules livrés avec Python s'appelle la bibliothèque standard. On dit souvent que Python est livré « piles incluses » (batteries included) : pas besoin d'installer quoi que ce soit pour tirer des nombres au hasard, manipuler des dates, lire du JSON ou calculer une racine carrée.
💡 Analogie : la bibliothèque standard, c'est une boîte à outils fournie avec ta maison. Un module, c'est un tiroir de cette boîte (le tiroir « dates », le tiroir « hasard »…).
import, c'est ouvrir le tiroir pour y prendre ce dont tu as besoin.
Tu en as déjà utilisé sans le savoir : json et csv au chapitre 07, pathlib aussi. Ce chapitre t'explique enfin ce qui se passe vraiment derrière import.
2. Importer un module¶
import module¶
La forme la plus simple et la plus courante :
import math charge le module et crée une variable math qui le représente. Pour utiliser son contenu, on écrit module.nom (notation pointée). L'avantage est énorme : en lisant math.sqrt, tu sais immédiatement d'où vient sqrt.
from module import nom¶
Si tu utilises souvent les mêmes fonctions, tu peux les importer directement :
Plus court à écrire, mais on perd l'information « ça vient de math ». Et attention : seuls les noms listés sont importés. math lui-même n'existe pas dans ce programme.
Renommer avec as¶
Tu peux donner un alias à un module ou à une fonction :
import statistics as stats
from datetime import datetime as dt
print(stats.mean([12, 15, 9, 18]))
print(type(dt.now()).__name__)
Certains alias sont des conventions universelles que tu rencontreras au chapitre 14 : import pandas as pd, import numpy as np.
from module import * : à éviter¶
L'étoile importe tout le contenu du module. Problème : tu ne sais plus d'où vient chaque nom, et si deux modules ont une fonction du même nom, la seconde écrase la première en silence. Réserve cette forme à l'interpréteur interactif pour tester rapidement.
Quelle forme choisir ?¶
| Forme | Quand l'utiliser |
|---|---|
import math |
Par défaut. Clair et sans conflit. |
from math import sqrt |
Pour 1 à 3 noms utilisés très souvent. |
import statistics as stats |
Pour un nom de module long, ou une convention connue. |
from math import * |
Jamais dans un vrai programme. |
Convention (PEP 8, voir chapitre 13) : les import se placent tout en haut du fichier, un module par ligne, dans cet ordre : bibliothèque standard, puis bibliothèques installées, puis tes propres modules, séparés par une ligne vide.
⚠️ Cet extrait suppose qu'un fichier
mes_outils.pyexiste : il illustre seulement l'ordre des imports.
3. Explorer un module : dir(), help() et la documentation¶
Comment savoir ce que contient un module ? Trois outils :
dir(module) liste tout ce qu'il contient :
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh']
Les noms entourés de doubles tirets bas (__doc__…) sont des éléments techniques ; ignore-les pour l'instant.
help(fonction) affiche la documentation (la docstring, vue au chapitre 05). Essaie dans l'interpréteur interactif (python dans le terminal) :
>>> import math
>>> help(math.sqrt)
Help on built-in function sqrt in module math:
sqrt(x, /)
Return the square root of x.
Appuie sur q pour quitter l'aide.
La documentation officielle, en français : https://docs.python.org/fr/3/library/index.html. Chaque module y a sa page, avec des exemples. Prends l'habitude d'y aller : c'est la référence. Dans VS Code, survoler une fonction ou faire Ctrl+clic dessus affiche aussi sa documentation.
4. Créer ton propre module¶
Créer un module, c'est… créer un fichier .py. Rien de plus. Prenons un dossier de projet avec deux fichiers côte à côte :
geometrie.py :
"""Petites fonctions de géométrie."""
import math
PI_ARRONDI = 3.14
def aire_cercle(rayon):
"""Renvoie l'aire d'un cercle."""
return math.pi * rayon**2
def perimetre_rectangle(longueur, largeur):
"""Renvoie le périmètre d'un rectangle."""
return 2 * (longueur + largeur)
main.py :
import geometrie
print(f"{geometrie.aire_cercle(2):.2f}")
print(geometrie.perimetre_rectangle(3, 5))
print(geometrie.PI_ARRONDI)
En lançant python main.py depuis le dossier mon_projet :
Points importants :
- On écrit
import geometriesans l'extension.py. - Le nom du fichier doit être un nom de variable valide : lettres minuscules, chiffres, tirets bas.
mon-module.pyou2outils.pyne pourront pas être importés (import mon-moduleprovoque uneSyntaxError). - Le module a sa propre « boîte » de noms : la variable
PI_ARRONDIdegeometriene se mélange pas avec celles demain.py. On parle d'espace de noms. - Un module peut lui-même importer d'autres modules (
geometrieimportemath).
Où Python cherche-t-il les modules ?¶
Quand tu écris import geometrie, Python cherche dans l'ordre :
- le dossier du script lancé (ici
mon_projet/) ; - les dossiers de la bibliothèque standard ;
- les dossiers des bibliothèques installées avec
pip(voir chapitre 12).
Il prend le premier fichier trouvé. Tu peux voir cette liste de dossiers avec sys.path :
Le premier dossier de la liste est celui de ton script. Retiens bien ce point, il explique le piège le plus célèbre de ce chapitre (fichier nommé random.py, voir les pièges).
Un module n'est exécuté qu'une fois¶
Lors du premier import, Python exécute tout le fichier du module, de haut en bas, puis le garde en mémoire. Les import suivants du même module (dans le même programme) réutilisent la version déjà chargée, sans le réexécuter.
5. if __name__ == "__main__":¶
Puisque import exécute tout le fichier, que se passe-t-il si ton module contient du code de test ?
temperatures.py :
def celsius_vers_fahrenheit(celsius):
return celsius * 9 / 5 + 32
print("Test :", celsius_vers_fahrenheit(100))
meteo.py :
La ligne Test : 212.0 s'affiche alors qu'on ne l'a pas demandée : elle vient de l'exécution de temperatures.py au moment de l'import. Gênant.
La solution est une variable spéciale, __name__, que Python crée automatiquement dans chaque fichier :
- si le fichier est lancé directement (
python temperatures.py),__name__vaut"__main__"; - si le fichier est importé,
__name__vaut le nom du module ("temperatures").
D'où ce schéma, que tu verras dans presque tous les fichiers Python :
def celsius_vers_fahrenheit(celsius):
return celsius * 9 / 5 + 32
if __name__ == "__main__":
# Ce bloc ne s'exécute QUE si on lance ce fichier directement
print("Test :", celsius_vers_fahrenheit(100))
Maintenant :
python temperatures.pyafficheTest : 212.0;python meteo.pyaffiche seulementIl fait 68.0 °F.
💡 Analogie : un module, c'est comme un chef cuisinier. Quand on l'invite dans une autre cuisine (import), il apporte ses recettes mais ne se met pas à cuisiner tout seul. Quand il est dans sa propre cuisine (lancé directement), il peut faire une démonstration.
Une organisation très répandue consiste à regrouper le programme principal dans une fonction main() :
def saluer(nom):
return f"Bonjour {nom} !"
def main():
nom = input("Ton prénom : ")
print(saluer(nom))
if __name__ == "__main__":
main()
Avec l'entrée Léa :
Ainsi, un autre fichier peut faire from salutations import saluer sans déclencher le input().
6. Les paquets : des modules rangés dans des dossiers¶
Quand tu as beaucoup de modules, tu peux les regrouper dans un dossier : c'est un paquet (package). On y ajoute traditionnellement un fichier (souvent vide) nommé __init__.py, qui indique « ce dossier est un paquet Python ».
outils/des.py :
outils/affichage.py :
def cadre(texte):
bordure = "+" + "-" * (len(texte) + 2) + "+"
return f"{bordure}\n| {texte} |\n{bordure}"
main.py :
from outils.des import lancer_de
from outils import affichage
resultat = lancer_de()
print(affichage.cadre(f"Tu as fait {resultat}"))
Exemple de sortie (le nombre change à chaque exécution) :
On accède à un module d'un paquet avec un point : outils.des, comme un chemin de dossier. Tu peux imbriquer des paquets dans des paquets. La structure complète d'un projet est détaillée au chapitre 13.
7. random : le hasard¶
Le module random génère des nombres pseudo-aléatoires (calculés par un algorithme, mais imprévisibles en pratique).
import random
print(random.randint(1, 6)) # entier entre 1 et 6, bornes INCLUSES
print(random.random()) # décimal dans [0, 1[
print(random.uniform(1.5, 3.5)) # décimal entre 1.5 et 3.5
couleurs = ["rouge", "vert", "bleu", "jaune"]
print(random.choice(couleurs)) # un élément au hasard
print(random.choices(couleurs, k=5)) # 5 tirages AVEC remise (doublons possibles)
print(random.sample(range(1, 50), 6)) # 6 tirages SANS remise (loto)
cartes = ["As", "Roi", "Dame", "Valet", "10"]
random.shuffle(cartes) # mélange la liste SUR PLACE (renvoie None)
print(cartes)
Exemple de sortie (différente à chaque exécution) :
6
0.16931005030681068
2.6574891618278063
bleu
['jaune', 'bleu', 'jaune', 'jaune', 'jaune']
[7, 34, 25, 47, 17, 40]
['Dame', 'As', '10', 'Valet', 'Roi']
⚠️ Contrairement à
range(1, 6)qui s'arrête à 5,randint(1, 6)inclut le 6. C'est une exception à la règle habituelle de Python.
Tirage pondéré¶
choices accepte des poids : ici « pile » a 9 fois plus de chances de sortir que « face ».
Rendre le hasard reproductible : seed¶
Pour déboguer ou tester, on veut parfois le même hasard à chaque exécution. random.seed(n) fixe le point de départ de l'algorithme :
import random
random.seed(2024)
print([random.randint(1, 100) for _ in range(5)])
random.seed(2024)
print([random.randint(1, 100) for _ in range(5)])
Même graine, mêmes nombres. Très pratique pour écrire des tests.
⚠️ Sécurité :
randomest prévisible si on connaît la graine. Pour des mots de passe ou des jetons secrets, utilise le modulesecrets, conçu pour la cryptographie :secrets.choice(...),secrets.randbelow(n),secrets.token_hex(16). On s'en servira dans le mini-projet.
8. math : les mathématiques¶
import math
print(math.pi, math.e)
print(math.sqrt(2)) # racine carrée
print(math.floor(3.7), math.ceil(3.2)) # arrondi inférieur / supérieur
print(math.floor(-3.7), int(-3.7)) # attention aux négatifs !
print(math.factorial(5)) # 5! = 120
print(math.gcd(12, 18), math.lcm(4, 6)) # PGCD et PPCM
print(math.hypot(3, 4)) # hypoténuse : racine de 3² + 4²
print(math.dist((0, 0), (3, 4))) # distance entre deux points
print(math.log10(1000), math.log2(8)) # logarithmes
print(math.cos(math.pi)) # trigonométrie (en radians)
print(math.degrees(math.pi / 2), math.radians(180))
3.141592653589793 2.718281828459045
1.4142135623730951
3 4
-4 -3
120
6 12
5.0
5.0
3.0 3.0
-1.0
90.0 3.141592653589793
math.floor arrondit vers le bas (vers moins l'infini) alors que int() tronque (supprime la partie décimale) : pour -3.7, on obtient -4 et -3.
Comparer des décimaux : math.isclose¶
Tu te souviens de l'imprécision des float (chapitre 01) ? math.isclose permet de comparer deux décimaux « à peu près » :
En bonus : statistics¶
Pour les calculs statistiques simples, inutile de tout recoder :
import statistics
notes = [12, 15, 9, 18]
print(statistics.mean(notes)) # moyenne
print(statistics.median(notes)) # médiane
print(round(statistics.stdev(notes), 2)) # écart-type
print(statistics.mode([1, 2, 2, 3])) # valeur la plus fréquente
9. datetime : dates et heures¶
Le module datetime contient plusieurs types (attention, l'un d'eux porte le même nom que le module !) :
| Type | Représente | Exemple |
|---|---|---|
date |
une date (jour, mois, année) | 14 juillet 2025 |
datetime |
une date et une heure | 31/12/2025 à 23:59:30 |
timedelta |
une durée | 30 jours, 2 heures |
On les importe généralement ainsi :
Créer et lire une date¶
from datetime import date, datetime
fete = date(2025, 7, 14) # année, mois, jour
print(fete)
print(fete.year, fete.month, fete.day)
print(fete.weekday()) # 0 = lundi, 6 = dimanche
reveillon = datetime(2025, 12, 31, 23, 59, 30)
print(reveillon)
print(reveillon.hour, reveillon.minute)
print(type(date.today()).__name__) # la date du jour
print(type(datetime.now()).__name__) # la date et l'heure actuelles
Calculer avec les dates : timedelta¶
Additionner une durée à une date, ou soustraire deux dates, c'est là que datetime brille. Il gère pour toi les mois de 28, 30 ou 31 jours et les années bissextiles :
from datetime import date, datetime, timedelta
fete = date(2025, 7, 14)
print(fete + timedelta(days=30)) # dans 30 jours
print(fete - timedelta(weeks=2)) # il y a 2 semaines
ecart = date(2025, 12, 25) - fete # date - date = timedelta
print(ecart)
print(ecart.days)
print(date(2024, 2, 28) + timedelta(days=1)) # 2024 est bissextile
print(date(2025, 2, 28) + timedelta(days=1))
duree = datetime(2026, 1, 1, 10, 0) - datetime(2025, 12, 31, 22, 30)
print(duree, "soit", duree.total_seconds(), "secondes")
Formater et lire du texte : strftime / strptime¶
strftime(string format time) : date → texte, selon un format.strptime(string parse time) : texte → date, selon un format.
| Code | Signification | Exemple |
|---|---|---|
%d |
jour sur 2 chiffres | 03 |
%m |
mois sur 2 chiffres | 02 |
%Y |
année sur 4 chiffres | 2026 |
%H |
heure (00-23) | 14 |
%M |
minutes | 30 |
%S |
secondes | 05 |
%A / %B |
nom du jour / du mois (en anglais par défaut) | Monday / July |
from datetime import date, datetime
fete = date(2025, 7, 14)
print(fete.strftime("%d/%m/%Y"))
rdv = datetime.strptime("03/02/2026 14:30", "%d/%m/%Y %H:%M")
print(rdv)
print(rdv.strftime("Rendez-vous le %d/%m à %Hh%M"))
print(date.fromisoformat("2026-02-03")) # format international AAAA-MM-JJ
Les noms de jours et de mois (%A, %B) sortent en anglais par défaut. La façon la plus simple et la plus fiable d'avoir le français est d'utiliser ta propre liste :
from datetime import date
JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet",
"août", "septembre", "octobre", "novembre", "décembre"]
fete = date(2025, 7, 14)
print(f"{JOURS[fete.weekday()]} {fete.day} {MOIS[fete.month - 1]} {fete.year}")
💡 Pour stocker une date dans un fichier JSON ou CSV, utilise le format ISO (
date.isoformat()→"2025-07-14") et relis-le avecdate.fromisoformat(). Ce format se trie correctement par ordre alphabétique !
10. os et sys : parler au système¶
os : le système d'exploitation¶
import os
print(os.getcwd()) # dossier de travail actuel
print(os.listdir(".")) # contenu d'un dossier
print(os.environ.get("HOME")) # variable d'environnement (None si absente)
print(os.sep) # séparateur de chemin : / ou \
La sortie dépend de ton ordinateur. Par exemple sous Linux :
Le module os contient aussi des fonctions pour créer ou supprimer des dossiers (os.makedirs, os.remove, os.rename…). Mais pour manipuler des chemins et fichiers, préfère pathlib (vu au chapitre 07), plus moderne et plus lisible :
Avec os |
Avec pathlib |
|---|---|
os.getcwd() |
Path.cwd() |
os.listdir(d) |
Path(d).iterdir() |
os.path.exists(c) |
Path(c).exists() |
os.path.join(d, "a.txt") |
Path(d) / "a.txt" |
os.makedirs(d, exist_ok=True) |
Path(d).mkdir(parents=True, exist_ok=True) |
os reste utile pour les variables d'environnement (os.environ), très utilisées pour stocker des réglages ou des mots de passe en dehors du code.
sys : l'interpréteur Python¶
import sys
print(sys.version_info >= (3, 12)) # vérifier la version de Python
print(sys.argv) # arguments de la ligne de commande
sys.argv est une liste contenant le nom du script suivi des arguments tapés dans le terminal. Fichier bonjour.py :
import sys
if len(sys.argv) < 2:
print("Usage : python bonjour.py PRENOM")
sys.exit(1) # arrête le programme avec un code d'erreur
prenom = sys.argv[1]
print(f"Bonjour {prenom} !")
sys.exit() arrête immédiatement le programme. Par convention, le code 0 signifie « tout va bien » et tout autre nombre signale une erreur. C'est la base des scripts d'automatisation du chapitre 14.
11. collections : des structures de données en plus¶
Le module collections offre des versions « améliorées » des listes et dictionnaires du chapitre 04.
Counter : compter¶
Compter des éléments est si fréquent qu'il existe un outil dédié :
from collections import Counter
lettres = Counter("abracadabra")
print(lettres)
print(lettres["a"], lettres["z"]) # pas de KeyError : 0 si absent
print(lettres.most_common(2)) # les 2 plus fréquents
mots = "le chat et le chien et le lapin".split()
print(Counter(mots).most_common(3))
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
5 0
[('a', 5), ('b', 2)]
[('le', 3), ('et', 2), ('chat', 1)]
Compare avec la version « à la main » du chapitre 04 : une boucle, un if mot in compteur, une incrémentation… Counter fait tout en une ligne.
defaultdict : un dictionnaire avec valeur par défaut¶
Pour regrouper des éléments, on écrit souvent : « si la clé n'existe pas, crée une liste vide, puis ajoute ». defaultdict le fait automatiquement :
from collections import defaultdict
eleves = [("Alice", "A"), ("Bob", "B"), ("Chloé", "A"), ("David", "C")]
par_classe = defaultdict(list) # valeur par défaut : une liste vide
for nom, classe in eleves:
par_classe[classe].append(nom) # pas besoin de vérifier si la clé existe
print(dict(par_classe))
On passe à defaultdict le type de la valeur par défaut, sans parenthèses : list (liste vide), int (0), set (ensemble vide), str (chaîne vide).
namedtuple : des tuples avec des noms¶
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p)
print(p.x, p.y) # accès par nom : plus lisible que p[0]
print(p[0]) # l'accès par indice marche toujours
C'est un premier pas vers les classes du chapitre 10 et les dataclasses du chapitre 11, qui feront la même chose en plus puissant.
deque : une file à double entrée¶
deque (prononcé « dèque ») est une liste optimisée pour ajouter et retirer aux deux bouts. Avec maxlen, elle ne garde que les N derniers éléments, idéal pour un historique :
from collections import deque
derniers = deque(maxlen=3)
for numero in range(1, 6):
derniers.append(numero)
print(list(derniers))
12. D'autres modules utiles à connaître¶
Tu n'as pas besoin de tout apprendre par cœur. Retiens surtout qu'ils existent, pour penser à les chercher le jour venu :
| Module | Pour quoi faire | Exemple |
|---|---|---|
time |
pauses, mesurer une durée | time.sleep(1), time.perf_counter() |
string |
constantes de caractères | string.ascii_letters, string.digits |
secrets |
hasard sécurisé | secrets.choice(...), secrets.token_hex(8) |
statistics |
moyenne, médiane… | statistics.median(...) |
json, csv |
formats de fichiers | vus au chapitre 07 |
pathlib, shutil |
fichiers, copies de dossiers | shutil.copy(...) |
itertools |
combinaisons, permutations | itertools.permutations("abc") |
textwrap |
mise en forme de texte | textwrap.wrap(texte, 40) |
urllib.request |
télécharger une page web | (on préférera requests, chapitre 14) |
import string
import time
print(string.ascii_lowercase)
print(string.digits)
debut = time.perf_counter()
total = sum(range(1_000_000))
duree = time.perf_counter() - debut
print(f"Calcul effectué en {duree:.4f} s")
Exemple de sortie (la durée varie selon l'ordinateur) :
Et quand la bibliothèque standard ne suffit pas, il existe plus de 500 000 bibliothèques supplémentaires sur PyPI, qu'on installe avec pip : ce sera le sujet du chapitre 12.
Pièges fréquents¶
Piège 1 — Nommer son fichier comme un module de la bibliothèque standard¶
C'est le piège classique. Tu veux tester le module random, alors tu crées un fichier… random.py :
Traceback (most recent call last):
File "/home/toi/cours/random.py", line 1, in <module>
import random
File "/home/toi/cours/random.py", line 3, in <module>
print(random.randint(1, 6))
^^^^^^^^^^^^^^
AttributeError: module 'random' has no attribute 'randint' (consider renaming '/home/toi/cours/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)
Pourquoi ? Python cherche d'abord dans le dossier du script (section 4) : import random importe donc… ton propre fichier, qui n'a pas de fonction randint. Python 3.13 te donne même la solution dans le message (consider renaming). En Python 3.12, le message est moins clair : AttributeError: partially initialized module 'random' has no attribute 'randint' (most likely due to a circular import).
✅ Correction : renomme ton fichier (test_hasard.py, mon_random.py…) et supprime le dossier __pycache__ s'il a été créé à côté. Noms à ne jamais donner à tes fichiers : random.py, math.py, json.py, csv.py, string.py, time.py, datetime.py, email.py, test.py, turtle.py…
Piège 2 — L'import circulaire¶
Deux modules qui s'importent mutuellement :
# fichier : joueurs.py
from scores import meilleur_score
def creer_joueur(nom):
return {"nom": nom, "score": 0}
# fichier : scores.py
from joueurs import creer_joueur
def meilleur_score(joueurs):
return max(joueur["score"] for joueur in joueurs)
En lançant un main.py qui contient from joueurs import creer_joueur, en Python 3.12 :
Traceback (most recent call last):
File "/home/toi/cours/main.py", line 1, in <module>
from joueurs import creer_joueur
File "/home/toi/cours/joueurs.py", line 1, in <module>
from scores import meilleur_score
File "/home/toi/cours/scores.py", line 1, in <module>
from joueurs import creer_joueur
ImportError: cannot import name 'creer_joueur' from partially initialized module 'joueurs' (most likely due to a circular import) (/home/toi/cours/joueurs.py)
(En Python 3.13, la dernière ligne est formulée autrement : ImportError: cannot import name 'creer_joueur' from 'joueurs' (consider renaming ...). Ne te laisse pas tromper : il ne s'agit pas d'un conflit de nom ici, mais bien d'un import circulaire. Le signe qui ne trompe pas : dans le traceback, le même fichier joueurs.py apparaît au début et est réimporté à la fin.)
Python commence à exécuter joueurs.py, qui demande scores.py, qui redemande joueurs.py… lequel n'est pas encore fini de charger (partially initialized) : creer_joueur n'existe pas encore.
✅ Corrections, de la meilleure à la moins bonne :
- Réorganiser : souvent, l'un des deux modules n'a pas vraiment besoin de l'autre. Ici
joueurs.pyn'utilise même pasmeilleur_score: on supprime l'import. - Déplacer le code commun dans un troisième module que les deux importent.
- En dernier recours, déplacer l'
importà l'intérieur de la fonction qui en a besoin.
Piège 3 — Confondre le module datetime et le type datetime¶
Ici, datetime désigne le module ; le type est à l'intérieur : datetime.datetime.now(). ✅ Plus lisible :
Piège 4 — Oublier le préfixe du module (ou l'import)¶
Avec import math, il faut écrire math.sqrt(16). ✅ Ou bien from math import sqrt.
Piège 5 — Mettre l'extension ou un tiret dans l'import¶
Python croit que outils est un paquet contenant un module py ! ✅ import outils. De même, un fichier mon-module.py ne peut pas être importé (le tiret est lu comme une soustraction) : nomme-le mon_module.py.
Piège 6 — ModuleNotFoundError parce qu'on n'est pas dans le bon dossier¶
Le fichier existe pourtant bien ! Causes fréquentes :
geometrie.pyn'est pas dans le même dossier que le script lancé ;- faute de frappe dans le nom (
geometrie/geometry) ; - pour une bibliothèque externe : elle n'est pas installée dans l'environnement utilisé (voir chapitre 12).
✅ Vérifie avec print(sys.path) où Python cherche, et place tes modules à côté de ton script principal.
Piège 7 — random.shuffle renvoie None¶
shuffle mélange la liste sur place et ne renvoie rien (comme liste.sort()). En réaffectant, on a écrasé la liste par None. ✅ Écris juste random.shuffle(cartes), ou utilise random.sample(cartes, len(cartes)) pour obtenir une nouvelle liste mélangée.
Piège 8 — Utiliser random pour des mots de passe¶
random est conçu pour les simulations et les jeux, pas pour la sécurité : ses tirages sont reproductibles par quelqu'un qui connaît son état interne. ✅ Pour tout ce qui est secret (mots de passe, jetons, codes de validation), utilise secrets.
Exercices¶
Exercice 1 ⭐ — Lancer de dés¶
Écris un programme qui simule le lancer de deux dés à six faces, affiche chaque dé et leur somme, et affiche Double ! si les deux dés sont identiques.
Exercice 2 ⭐ — Calculs géométriques¶
En utilisant le module math, écris trois fonctions :
aire_disque(rayon);hypotenuse(a, b): la longueur de l'hypoténuse d'un triangle rectangle ;nombre_de_dalles(surface_m2, surface_dalle_m2): le nombre de dalles à acheter (on ne peut pas acheter une fraction de dalle, donc on arrondit au-dessus).
print(f"{aire_disque(3):.2f}") # 28.27
print(hypotenuse(6, 8)) # 10.0
print(nombre_de_dalles(10.5, 0.36)) # 30
Exercice 3 ⭐ — Combien de jours ?¶
Écris une fonction jours_avant(jour_cible, aujourd_hui) qui reçoit deux objets date et renvoie le nombre de jours qui les séparent. Puis écris une fonction prochain_anniversaire(naissance, aujourd_hui) qui renvoie le nombre de jours avant le prochain anniversaire (0 si c'est aujourd'hui).
aujourd_hui = date(2026, 3, 10)
print(jours_avant(date(2026, 12, 25), aujourd_hui)) # 290
print(prochain_anniversaire(date(1998, 5, 1), aujourd_hui)) # 52
print(prochain_anniversaire(date(2001, 1, 15), aujourd_hui)) # 311
print(prochain_anniversaire(date(1990, 3, 10), aujourd_hui)) # 0
(On ignore le cas des personnes nées un 29 février.)
Exercice 4 ⭐⭐ — Analyse d'un texte avec Counter¶
Écris une fonction analyser(texte) qui affiche :
- le nombre total de mots ;
- le nombre de mots différents ;
- les 3 mots les plus fréquents (en minuscules, ponctuation
.,;:!?retirée) ; - la lettre la plus fréquente (on ne compte que les lettres).
Exercice 5 ⭐⭐ — Ton premier module¶
Crée un module conversions.py contenant :
km_vers_miles(km)(1 mile = 1,609344 km) ;celsius_vers_fahrenheit(c);secondes_vers_hms(secondes)qui renvoie une chaîne"HH:MM:SS".
Le module doit contenir un bloc if __name__ == "__main__": qui vérifie les trois fonctions avec des assert et affiche Tous les tests sont passés.
Crée ensuite un fichier principal.py qui importe le module et affiche :
Vérifie que lancer principal.py n'affiche pas le message des tests.
Exercice 6 ⭐⭐ — Répertoire par initiale¶
Avec defaultdict, écris une fonction regrouper_par_initiale(noms) qui renvoie un dictionnaire associant chaque initiale (en majuscule) à la liste triée des noms qui commencent par cette lettre. Les clés doivent elles aussi être triées.
noms = ["léa", "Hugo", "lucas", "Emma", "hugo", "Inès", "Ethan", "Louise"]
print(regrouper_par_initiale(noms))
(Les noms sont remis en forme avec .capitalize().)
Exercice 7 ⭐⭐⭐ — Simulation statistique¶
Simule 10 000 lancers de deux dés et compte, avec Counter, combien de fois chaque somme (2 à 12) apparaît. Affiche un histogramme horizontal, où chaque # représente 50 lancers, avec le pourcentage. Utilise random.seed(42) pour obtenir des résultats reproductibles.
Question bonus : pourquoi le 7 est-il la somme la plus fréquente ?
Exercice 8 ⭐⭐⭐ — Un outil en ligne de commande¶
Écris un script compter.py qui s'utilise ainsi dans le terminal :
$ python compter.py
Usage : python compter.py FICHIER [NOMBRE]
$ python compter.py absent.txt
Erreur : le fichier 'absent.txt' n'existe pas.
$ python compter.py poeme.txt 3
poeme.txt : 6 lignes, 26 mots
Les 3 mots les plus fréquents :
la : 4
mer : 3
le : 2
- Le premier argument est le chemin d'un fichier texte, le second (optionnel, 5 par défaut) le nombre de mots à afficher.
- Utilise
sys.argv,sys.exit,pathlibetCounter. - Si le second argument n'est pas un entier, affiche une erreur claire.
- Organise ton code avec une fonction
main()etif __name__ == "__main__":.
Fichier poeme.txt de test :
La mer, la mer, toujours recommencée.
Le vent se lève, il faut tenter de vivre.
La mer brille et le ciel
s'endort et
la vague
chante.
Mini-projet — Générateur de mots de passe et module utilitaire maison¶
Cahier des charges¶
Tu vas construire un petit projet sur deux fichiers :
generateur_mdp/
├── mdp_outils.py ← ton module utilitaire réutilisable
└── generateur.py ← le programme principal (menu)
Le module mdp_outils.py doit fournir :
generer_mot_de_passe(longueur=12, majuscules=True, chiffres=True, symboles=True): renvoie un mot de passe aléatoire sécurisé (modulesecrets), contenant au moins un caractère de chaque catégorie demandée (les minuscules sont toujours incluses). Lève uneValueErrorsi la longueur est inférieure à 4 ou au nombre de catégories.generer_phrase_de_passe(nombre_mots=4, separateur="-"): assemble des mots tirés d'une liste (au moins 20 mots) et ajoute un nombre à 2 chiffres à la fin. Exemple :tigre-nuage-velo-orange-42.evaluer_force(mot_de_passe): renvoie un tuple(score, verdict)où le score (0 à 5) compte un point pour : longueur ≥ 8, longueur ≥ 12, présence de majuscules, de chiffres, de symboles. Verdict :"faible"(0-2),"moyen"(3),"fort"(4-5).- Un bloc
if __name__ == "__main__":avec des tests (assert).
Le programme generateur.py affiche un menu en boucle :
=== Générateur de mots de passe ===
1. Mot de passe aléatoire
2. Phrase de passe
3. Tester la force d'un mot de passe
4. Voir l'historique de la session
5. Quitter
Ton choix :
- Le choix 1 demande la longueur et les options (o/n) ; les saisies invalides sont gérées (chapitre 08 !).
- Les choix 1 et 2 affichent le résultat avec sa force, et l'ajoutent à l'historique avec l'heure (
datetime). - L'historique ne garde que les 5 dernières générations (
deque).
Étapes suggérées¶
- Commence par
mdp_outils.pyet sa fonctionevaluer_force(la plus simple), avec desassertdans le bloc__main__. Lance le module seul pour tester. - Écris
generer_mot_de_passe: construis l'alphabet autorisé avecstring.ascii_lowercase,string.ascii_uppercase,string.digits,string.punctuation. Tire d'abord un caractère obligatoire par catégorie, complète avec des caractères tirés dans l'alphabet complet, puis mélange le tout (secrets.SystemRandom().shuffle). - Écris
generer_phrase_de_passe. - Écris
generateur.py: une fonction par choix du menu, une fonctiondemander_entier(message, minimum, maximum)et une fonctiondemander_oui_non(message). - Ajoute l'historique avec
deque(maxlen=5)etdatetime.now().strftime("%H:%M:%S").
Pistes d'amélioration¶
- Option « éviter les caractères ambigus » (
l,1,I,O,0). - Copier automatiquement le mot de passe dans le presse-papiers (bibliothèque externe
pyperclip, voir chapitre 12). - Lire la liste de mots de la phrase de passe dans un fichier texte.
- Accepter des arguments en ligne de commande (
python generateur.py --longueur 20) avec le moduleargparse.
Corrigés¶
Corrigé de l'exercice 1
Exemple de sortie (différente à chaque exécution) : `randint(1, 6)` inclut bien le 1 **et** le 6. Pour tester le cas « Double ! », tu peux temporairement ajouter `random.seed(...)` en essayant plusieurs graines, ou forcer `de2 = de1`.Corrigé de l'exercice 2
import math
def aire_disque(rayon):
return math.pi * rayon**2
def hypotenuse(a, b):
return math.hypot(a, b) # ou math.sqrt(a**2 + b**2)
def nombre_de_dalles(surface_m2, surface_dalle_m2):
return math.ceil(surface_m2 / surface_dalle_m2)
print(f"{aire_disque(3):.2f}")
print(hypotenuse(6, 8))
print(nombre_de_dalles(10.5, 0.36))
Corrigé de l'exercice 3
from datetime import date
def jours_avant(jour_cible, aujourd_hui):
return (jour_cible - aujourd_hui).days
def prochain_anniversaire(naissance, aujourd_hui):
anniversaire = date(aujourd_hui.year, naissance.month, naissance.day)
if anniversaire < aujourd_hui: # déjà passé cette année
anniversaire = date(aujourd_hui.year + 1, naissance.month, naissance.day)
return jours_avant(anniversaire, aujourd_hui)
aujourd_hui = date(2026, 3, 10)
print(jours_avant(date(2026, 12, 25), aujourd_hui))
print(prochain_anniversaire(date(1998, 5, 1), aujourd_hui))
print(prochain_anniversaire(date(2001, 1, 15), aujourd_hui))
print(prochain_anniversaire(date(1990, 3, 10), aujourd_hui))
Corrigé de l'exercice 4
from collections import Counter
def analyser(texte):
mots = [mot.strip(".,;:!?").lower() for mot in texte.split()]
mots = [mot for mot in mots if mot] # retire les « mots » vides (ex : « ! » seul)
compteur_mots = Counter(mots)
lettres = Counter(c for c in texte.lower() if c.isalpha())
lettre, _ = lettres.most_common(1)[0]
top = ", ".join(f"{mot} ({n})" for mot, n in compteur_mots.most_common(3))
print(f"Mots : {len(mots)}")
print(f"Mots différents : {len(compteur_mots)}")
print(f"Top 3 : {top}")
print(f"Lettre la plus fréquente : {lettre}")
analyser("Le petit chat boit du lait. Le chat dort. Le chien aboie !")
Corrigé de l'exercice 5
Fichier `conversions.py` :"""Fonctions de conversion d'unités."""
KM_PAR_MILE = 1.609344
def km_vers_miles(km):
return km / KM_PAR_MILE
def celsius_vers_fahrenheit(c):
return c * 9 / 5 + 32
def secondes_vers_hms(secondes):
heures, reste = divmod(secondes, 3600)
minutes, secondes = divmod(reste, 60)
return f"{heures:02d}:{minutes:02d}:{secondes:02d}"
if __name__ == "__main__":
assert round(km_vers_miles(1.609344), 6) == 1
assert celsius_vers_fahrenheit(100) == 212
assert celsius_vers_fahrenheit(-40) == -40
assert secondes_vers_hms(3661) == "01:01:01"
assert secondes_vers_hms(0) == "00:00:00"
print("Tous les tests sont passés.")
Corrigé de l'exercice 6
from collections import defaultdict
def regrouper_par_initiale(noms):
groupes = defaultdict(list)
for nom in noms:
nom = nom.capitalize()
groupes[nom[0]].append(nom)
return {initiale: sorted(groupes[initiale]) for initiale in sorted(groupes)}
noms = ["léa", "Hugo", "lucas", "Emma", "hugo", "Inès", "Ethan", "Louise"]
print(regrouper_par_initiale(noms))
Corrigé de l'exercice 7
import random
from collections import Counter
NOMBRE_LANCERS = 10_000
LANCERS_PAR_DIESE = 50
random.seed(42)
sommes = Counter(
random.randint(1, 6) + random.randint(1, 6) for _ in range(NOMBRE_LANCERS)
)
for somme in range(2, 13):
nombre = sommes[somme]
pourcentage = nombre / NOMBRE_LANCERS * 100
barre = "#" * (nombre // LANCERS_PAR_DIESE)
print(f"{somme:2} : {pourcentage:5.1f} % {barre}")
2 : 2.7 % #####
3 : 5.4 % ##########
4 : 8.2 % ################
5 : 10.9 % #####################
6 : 13.6 % ###########################
7 : 17.0 % ##################################
8 : 13.9 % ###########################
9 : 11.2 % ######################
10 : 8.4 % ################
11 : 5.6 % ###########
12 : 3.1 % ######
Corrigé de l'exercice 8
"""Compte les lignes et les mots d'un fichier texte."""
import sys
from collections import Counter
from pathlib import Path
PONCTUATION = ".,;:!?\"'()"
def compter_mots(texte):
mots = (mot.strip(PONCTUATION).lower() for mot in texte.split())
return Counter(mot for mot in mots if mot)
def main():
if len(sys.argv) < 2:
print("Usage : python compter.py FICHIER [NOMBRE]")
sys.exit(1)
chemin = Path(sys.argv[1])
nombre = 5
if len(sys.argv) >= 3:
try:
nombre = int(sys.argv[2])
except ValueError:
print(f"Erreur : {sys.argv[2]!r} n'est pas un nombre entier.")
sys.exit(1)
try:
texte = chemin.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"Erreur : le fichier '{chemin}' n'existe pas.")
sys.exit(1)
compteur = compter_mots(texte)
nb_lignes = len(texte.splitlines())
nb_mots = compteur.total()
print(f"{chemin.name} : {nb_lignes} lignes, {nb_mots} mots")
print(f"Les {nombre} mots les plus fréquents :")
for mot, occurrences in compteur.most_common(nombre):
print(f" {mot} : {occurrences}")
if __name__ == "__main__":
main()
$ python compter.py
Usage : python compter.py FICHIER [NOMBRE]
$ python compter.py absent.txt
Erreur : le fichier 'absent.txt' n'existe pas.
$ python compter.py poeme.txt trois
Erreur : 'trois' n'est pas un nombre entier.
$ python compter.py poeme.txt 3
poeme.txt : 6 lignes, 26 mots
Les 3 mots les plus fréquents :
la : 4
mer : 3
le : 2
Corrigé du mini-projet
Fichier `mdp_outils.py` :"""Outils de génération et d'évaluation de mots de passe."""
import secrets
import string
MOTS = [
"tigre", "nuage", "velo", "orange", "piano", "cactus", "fusee", "galaxie",
"pirate", "volcan", "biscuit", "dragon", "lune", "marmotte", "tornade",
"citron", "baleine", "chateau", "domino", "glacier", "hibou", "jungle",
"kayak", "licorne", "mangue", "neige", "olive", "pingouin", "requin", "tulipe",
]
_aleatoire = secrets.SystemRandom() # générateur sécurisé, avec les méthodes de random
def generer_mot_de_passe(longueur=12, majuscules=True, chiffres=True, symboles=True):
"""Renvoie un mot de passe aléatoire sécurisé.
Il contient toujours des minuscules, et au moins un caractère
de chaque catégorie demandée.
"""
categories = [string.ascii_lowercase]
if majuscules:
categories.append(string.ascii_uppercase)
if chiffres:
categories.append(string.digits)
if symboles:
categories.append(string.punctuation)
if longueur < max(4, len(categories)):
raise ValueError(f"longueur trop courte : {longueur} (minimum 4)")
alphabet = "".join(categories)
caracteres = [secrets.choice(categorie) for categorie in categories]
caracteres += [secrets.choice(alphabet) for _ in range(longueur - len(caracteres))]
_aleatoire.shuffle(caracteres) # sinon les caractères obligatoires seraient au début
return "".join(caracteres)
def generer_phrase_de_passe(nombre_mots=4, separateur="-"):
"""Renvoie une phrase de passe du type tigre-nuage-velo-orange-42."""
mots = [secrets.choice(MOTS) for _ in range(nombre_mots)]
nombre = secrets.randbelow(90) + 10 # entre 10 et 99
return separateur.join(mots) + separateur + str(nombre)
def evaluer_force(mot_de_passe):
"""Renvoie (score sur 5, verdict)."""
criteres = [
len(mot_de_passe) >= 8,
len(mot_de_passe) >= 12,
any(c.isupper() for c in mot_de_passe),
any(c.isdigit() for c in mot_de_passe),
any(c in string.punctuation for c in mot_de_passe),
]
score = sum(criteres) # True compte pour 1, False pour 0
if score <= 2:
verdict = "faible"
elif score == 3:
verdict = "moyen"
else:
verdict = "fort"
return score, verdict
if __name__ == "__main__":
assert evaluer_force("abc") == (0, "faible")
assert evaluer_force("Abcdefgh1") == (3, "moyen")
assert evaluer_force("Abcdefgh1!xyz") == (5, "fort")
for _ in range(100):
mdp = generer_mot_de_passe(10)
assert len(mdp) == 10
assert any(c.isupper() for c in mdp)
assert any(c.isdigit() for c in mdp)
assert any(c in string.punctuation for c in mdp)
mdp = generer_mot_de_passe(6, majuscules=False, chiffres=False, symboles=False)
assert mdp.islower() and len(mdp) == 6
try:
generer_mot_de_passe(3)
except ValueError:
pass
else:
raise AssertionError("une ValueError était attendue")
phrase = generer_phrase_de_passe(3, "_")
assert phrase.count("_") == 3
print("mdp_outils : tous les tests sont passés.")
print("Exemple :", generer_mot_de_passe(), "/", generer_phrase_de_passe())
"""Menu interactif de génération de mots de passe."""
from collections import deque
from datetime import datetime
import mdp_outils
historique = deque(maxlen=5)
def demander_entier(message, minimum, maximum):
while True:
try:
valeur = int(input(message))
except ValueError:
print("Merci d'entrer un nombre entier.")
continue
if minimum <= valeur <= maximum:
return valeur
print(f"Le nombre doit être entre {minimum} et {maximum}.")
def demander_oui_non(message):
while True:
reponse = input(f"{message} (o/n) : ").strip().lower()
if reponse in ("o", "oui"):
return True
if reponse in ("n", "non"):
return False
print("Réponds par o ou n.")
def afficher_et_memoriser(secret):
score, verdict = mdp_outils.evaluer_force(secret)
print(f"\n → {secret} (force : {verdict}, {score}/5)\n")
heure = datetime.now().strftime("%H:%M:%S")
historique.append(f"[{heure}] {secret}")
def choix_mot_de_passe():
longueur = demander_entier("Longueur (4 à 64) : ", 4, 64)
majuscules = demander_oui_non("Majuscules ?")
chiffres = demander_oui_non("Chiffres ?")
symboles = demander_oui_non("Symboles ?")
secret = mdp_outils.generer_mot_de_passe(longueur, majuscules, chiffres, symboles)
afficher_et_memoriser(secret)
def choix_phrase():
nombre_mots = demander_entier("Nombre de mots (3 à 8) : ", 3, 8)
afficher_et_memoriser(mdp_outils.generer_phrase_de_passe(nombre_mots))
def choix_tester():
mot_de_passe = input("Mot de passe à tester : ")
score, verdict = mdp_outils.evaluer_force(mot_de_passe)
print(f"Force : {verdict} ({score}/5)")
def choix_historique():
if not historique:
print("Aucune génération pour l'instant.")
for ligne in historique:
print(ligne)
def main():
actions = {
"1": choix_mot_de_passe,
"2": choix_phrase,
"3": choix_tester,
"4": choix_historique,
}
while True:
print("=== Générateur de mots de passe ===")
print("1. Mot de passe aléatoire")
print("2. Phrase de passe")
print("3. Tester la force d'un mot de passe")
print("4. Voir l'historique de la session")
print("5. Quitter")
choix = input("Ton choix : ").strip()
if choix == "5":
print("À bientôt !")
break
action = actions.get(choix)
if action is None:
print("Choix invalide.")
else:
action()
if __name__ == "__main__":
main()
=== Générateur de mots de passe ===
1. Mot de passe aléatoire
2. Phrase de passe
3. Tester la force d'un mot de passe
4. Voir l'historique de la session
5. Quitter
Ton choix : 1
Longueur (4 à 64) : 2
Le nombre doit être entre 4 et 64.
Longueur (4 à 64) : 16
Majuscules ? (o/n) : o
Chiffres ? (o/n) : peut-être
Réponds par o ou n.
Chiffres ? (o/n) : o
Symboles ? (o/n) : n
→ uJbMEcdEOm7yrS9X (force : fort, 4/5)
=== Générateur de mots de passe ===
...
Ton choix : 3
Mot de passe à tester : soleil
Force : faible (0/5)
...
Ton choix : 4
[14:02:31] uJbMEcdEOm7yrS9X
...
Ton choix : 5
À bientôt !
Récapitulatif¶
- Un module est un fichier
.py; un paquet est un dossier de modules (avec un__init__.py). La bibliothèque standard est l'ensemble des modules livrés avec Python. import modulepuismodule.nom(forme par défaut) ;from module import nompour quelques noms fréquents ;aspour un alias ; jamaisimport *.- Python cherche les modules d'abord dans le dossier du script : ne nomme jamais un fichier comme un module existant (
random.py,math.py…). - Un module est exécuté une fois, au premier import. Le code de test se place sous
if __name__ == "__main__":, souvent dans une fonctionmain(). random:randint(bornes incluses),choice,choices,sample,shuffle(sur place),seed. Pour la sécurité :secrets.math:sqrt,pi,floor,ceil,hypot,isclose… etstatisticspour moyenne, médiane, écart-type.datetime:date,datetime,timedelta;strftime(date → texte),strptime(texte → date),isoformat/fromisoformat.os(variables d'environnement, système),sys(argv,exit,path) ; préfèrepathlibpour les fichiers.collections:Counter(compter),defaultdict(regrouper),namedtuple(tuples nommés),deque(file, historique limité).- Pour explorer :
dir(),help(), et la documentation officielle https://docs.python.org/fr/3/library/.
Checklist de validation¶
- Je connais les différentes façons d'importer et je sais laquelle utiliser par défaut.
- J'ai créé un module à moi et je l'ai importé depuis un autre fichier.
- Je sais expliquer à quoi sert
if __name__ == "__main__":. - Je sais tirer un nombre au hasard, piocher dans une liste et rendre le hasard reproductible.
- Je sais calculer le nombre de jours entre deux dates et formater une date en
JJ/MM/AAAA. - Je sais compter des éléments avec
Counteret regrouper avecdefaultdict. - Je sais lire les arguments de la ligne de commande avec
sys.argv. - Je sais pourquoi mon fichier ne doit jamais s'appeler
random.py.