Aller au contenu

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 utiliser if __name__ == "__main__": ; - utiliser les modules incontournables de la bibliothèque standard : random, math, datetime, os, sys, collections ; - explorer un module inconnu avec dir(), 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

print(math.sqrt(16))
print(math.pi)
4.0
3.141592653589793

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 :

from math import sqrt, pi

print(sqrt(16))
print(pi)
4.0
3.141592653589793

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__)
13.5
datetime

Certains alias sont des conventions universelles que tu rencontreras au chapitre 14 : import pandas as pd, import numpy as np.

from module import * : à éviter

from math import *
from statistics import *

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.

import json
import random
from pathlib import Path

from mes_outils import nettoyer_texte

⚠️ Cet extrait suppose qu'un fichier mes_outils.py existe : 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 :

import math

print(dir(math)[:12])
['__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 :

mon_projet/
├── geometrie.py
└── main.py

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 :

12.57
16
3.14

Points importants :

  • On écrit import geometrie sans l'extension .py.
  • Le nom du fichier doit être un nom de variable valide : lettres minuscules, chiffres, tirets bas. mon-module.py ou 2outils.py ne pourront pas être importés (import mon-module provoque une SyntaxError).
  • Le module a sa propre « boîte » de noms : la variable PI_ARRONDI de geometrie ne se mélange pas avec celles de main.py. On parle d'espace de noms.
  • Un module peut lui-même importer d'autres modules (geometrie importe math).

Où Python cherche-t-il les modules ?

Quand tu écris import geometrie, Python cherche dans l'ordre :

  1. le dossier du script lancé (ici mon_projet/) ;
  2. les dossiers de la bibliothèque standard ;
  3. 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 :

import sys

for dossier in sys.path:
    print(dossier)

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 :

import temperatures

print(f"Il fait {temperatures.celsius_vers_fahrenheit(20)} °F")
Test : 212.0
Il fait 68.0 °F

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.py affiche Test : 212.0 ;
  • python meteo.py affiche seulement Il 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 :

Ton prénom : Léa
Bonjour 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 ».

jeu/
├── main.py
└── outils/
    ├── __init__.py
    ├── des.py
    └── affichage.py

outils/des.py :

import random


def lancer_de(faces=6):
    return random.randint(1, faces)

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) :

+--------------+
| Tu as fait 4 |
+--------------+

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 ».

import random

print(random.choices(["pile", "face"], weights=[9, 1], k=10))

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)])
[61, 24, 94, 75, 39]
[61, 24, 94, 75, 39]

Même graine, mêmes nombres. Très pratique pour écrire des tests.

⚠️ Sécurité : random est prévisible si on connaît la graine. Pour des mots de passe ou des jetons secrets, utilise le module secrets, 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 » :

import math

print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))
False
True

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
13.5
13.5
3.87
2

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 :

from datetime import date, datetime, timedelta

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
2025-07-14
2025 7 14
0
2025-12-31 23:59:30
23 59
date
datetime

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")
2025-08-13
2025-06-30
164 days, 0:00:00
164
2024-02-29
2025-03-01
11:30:00 soit 41400.0 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
14/07/2025
2026-02-03 14:30:00
Rendez-vous le 03/02 à 14h30
2026-02-03

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}")
lundi 14 juillet 2025

💡 Pour stocker une date dans un fichier JSON ou CSV, utilise le format ISO (date.isoformat() → "2025-07-14") et relis-le avec date.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 :

/home/toi/cours
['mon_script.py', 'notes.txt']
/home/toi
/

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} !")
$ python bonjour.py
Usage : python bonjour.py PRENOM
$ python bonjour.py Léa
Bonjour Léa !

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))
{'A': ['Alice', 'Chloé'], 'B': ['Bob'], 'C': ['David']}

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
Point(x=3, y=4)
3 4
3

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))
[1]
[1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]

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) :

abcdefghijklmnopqrstuvwxyz
0123456789
Calcul effectué en 0.0123 s

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 :

# fichier : random.py
import random

print(random.randint(1, 6))
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 :

  1. Réorganiser : souvent, l'un des deux modules n'a pas vraiment besoin de l'autre. Ici joueurs.py n'utilise même pas meilleur_score : on supprime l'import.
  2. Déplacer le code commun dans un troisième module que les deux importent.
  3. 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

import datetime

maintenant = datetime.now()
AttributeError: module 'datetime' has no attribute 'now'

Ici, datetime désigne le module ; le type est à l'intérieur : datetime.datetime.now(). ✅ Plus lisible :

from datetime import datetime

maintenant = datetime.now()

Piège 4 — Oublier le préfixe du module (ou l'import)

import math

print(sqrt(16))
NameError: name 'sqrt' is not defined

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

import outils.py
ModuleNotFoundError: No module named 'outils.py'; 'outils' is not a package

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

ModuleNotFoundError: No module named 'geometrie'

Le fichier existe pourtant bien ! Causes fréquentes :

  • geometrie.py n'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

import random

cartes = ["As", "Roi", "Dame"]
cartes = random.shuffle(cartes)
print(cartes)
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.

Dé 1 : 4
Dé 2 : 4
Somme : 8
Double !

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).
analyser("Le petit chat boit du lait. Le chat dort. Le chien aboie !")
Mots : 12
Mots différents : 9
Top 3 : le (3), chat (2), petit (1)
Lettre la plus fréquente : t

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 :

42.195 km = 26.22 miles
37 °C = 98.6 °F
5000 s = 01:23:20

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))
{'E': ['Emma', 'Ethan'], 'H': ['Hugo', 'Hugo'], 'I': ['Inès'], 'L': ['Louise', 'Lucas', 'Léa']}

(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.

 2 :   2.7 % #####
 3 :   5.4 % ##########
...
 7 :  17.0 % ##################################
...

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, pathlib et Counter.
  • Si le second argument n'est pas un entier, affiche une erreur claire.
  • Organise ton code avec une fonction main() et if __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 :

  1. generer_mot_de_passe(longueur=12, majuscules=True, chiffres=True, symboles=True) : renvoie un mot de passe aléatoire sécurisé (module secrets), contenant au moins un caractère de chaque catégorie demandée (les minuscules sont toujours incluses). Lève une ValueError si la longueur est inférieure à 4 ou au nombre de catégories.
  2. 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.
  3. 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).
  4. 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

  1. Commence par mdp_outils.py et sa fonction evaluer_force (la plus simple), avec des assert dans le bloc __main__. Lance le module seul pour tester.
  2. Écris generer_mot_de_passe : construis l'alphabet autorisé avec string.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).
  3. Écris generer_phrase_de_passe.
  4. Écris generateur.py : une fonction par choix du menu, une fonction demander_entier(message, minimum, maximum) et une fonction demander_oui_non(message).
  5. Ajoute l'historique avec deque(maxlen=5) et datetime.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 module argparse.

Corrigés

Corrigé de l'exercice 1
import random

de1 = random.randint(1, 6)
de2 = random.randint(1, 6)

print(f"Dé 1 : {de1}")
print(f"Dé 2 : {de2}")
print(f"Somme : {de1 + de2}")
if de1 == de2:
    print("Double !")
Exemple de sortie (différente à chaque exécution) :
Dé 1 : 3
Dé 2 : 5
Somme : 8
`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))
28.27
10.0
30
10,5 / 0,36 = 29,17 : avec 29 dalles, il manquerait un morceau, d'où `math.ceil` (arrondi supérieur). `round` aurait donné 29, et `int` aussi.
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))
290
52
311
0
Pourquoi passer `aujourd_hui` en paramètre plutôt que d'appeler `date.today()` dans la fonction ? Pour pouvoir **tester** la fonction avec une date fixe : le résultat est alors toujours le même. Dans le programme réel, on appellera `prochain_anniversaire(naissance, date.today())`. Les dates se comparent directement avec `<`.
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 !")
Mots : 12
Mots différents : 9
Top 3 : le (3), chat (2), petit (1)
Lettre la plus fréquente : t
Le `!` isolé devient une chaîne vide après `strip` : sans le filtre, on compterait 13 « mots ». `len(compteur_mots)` donne le nombre de clés, donc de mots **différents**. `most_common(1)` renvoie une liste contenant un tuple : `[('t', 6)]`, d'où le `[0]` puis le dépaquetage. À égalité, `most_common` garde l'ordre de première apparition (c'est pourquoi `petit` sort avant les autres mots vus une seule fois).
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.")
Fichier `principal.py`, dans le **même dossier** :
import conversions

print(f"42.195 km = {conversions.km_vers_miles(42.195):.2f} miles")
print(f"37 °C = {conversions.celsius_vers_fahrenheit(37):.1f} °F")
print(f"5000 s = {conversions.secondes_vers_hms(5000)}")
`python principal.py` :
42.195 km = 26.22 miles
37 °C = 98.6 °F
5000 s = 01:23:20
`python conversions.py` :
Tous les tests sont passés.
`divmod(a, b)` renvoie à la fois le quotient et le reste (`a // b`, `a % b`). Le format `:02d` affiche un entier sur 2 chiffres en complétant par des zéros. La constante `KM_PAR_MILE` est en majuscules : c'est la convention pour une valeur qui ne change jamais.
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))
{'E': ['Emma', 'Ethan'], 'H': ['Hugo', 'Hugo'], 'I': ['Inès'], 'L': ['Louise', 'Lucas', 'Léa']}
La compréhension de dictionnaire finale ([chapitre 06](06-chaines-et-comprehensions.md)) trie à la fois les clés et chaque liste, et transforme le `defaultdict` en `dict` ordinaire. Remarque que `Léa` arrive **après** `Lucas` : le tri compare les codes des caractères, et `é` a un code plus grand que toutes les lettres non accentuées. Trier « à la française » demanderait le module `locale` ou une clé de tri qui retire les accents.
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 % ######
On parcourt `range(2, 13)` plutôt que le `Counter` directement, pour avoir les sommes **dans l'ordre**. `{somme:2}` aligne à droite sur 2 caractères, `{pourcentage:5.1f}` sur 5 caractères avec 1 décimale. **Bonus** : il y a 36 combinaisons possibles de deux dés. Le 7 s'obtient de 6 façons (1+6, 2+5, 3+4, 4+3, 5+2, 6+1), soit 6/36 ≈ 16,7 %, alors que le 2 ne s'obtient que d'une façon (1+1), soit 1/36 ≈ 2,8 %. La simulation retrouve bien ces valeurs théoriques.
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
`Counter.total()` (Python 3.10+) renvoie la somme de tous les compteurs, c'est-à-dire le nombre total de mots. `le` et `et` apparaissent tous deux 2 fois : à égalité, `most_common` garde l'ordre de première apparition, donc `le` (ligne 2) passe avant `et` (ligne 3). Attention : `s'endort` reste un seul mot, car l'apostrophe n'est retirée qu'aux extrémités. Les messages d'erreur et `sys.exit(1)` permettent à d'autres scripts de savoir que quelque chose s'est mal passé.
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())
Fichier `generateur.py` :
"""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()
Exemple de session (les mots de passe changent à chaque exécution) :
=== 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 !
Points importants : - **Séparation des rôles** : `mdp_outils.py` ne fait **aucun** `print` ni `input` (hors tests). Il est donc réutilisable dans un autre programme, une application web, des tests… `generateur.py` ne s'occupe que du dialogue avec l'utilisateur. - Le **dictionnaire de fonctions** `actions` évite une longue chaîne de `if/elif` : les fonctions sont des valeurs comme les autres, qu'on peut ranger dans un dictionnaire puis appeler avec `action()`. - `secrets.SystemRandom()` donne accès aux méthodes habituelles de `random` (comme `shuffle`), mais avec la source de hasard sécurisée du système. - `sum(criteres)` fonctionne car `True` vaut 1 et `False` vaut 0. - `deque(maxlen=5)` oublie automatiquement les générations les plus anciennes. - La variable `_aleatoire` commence par un tiret bas : c'est la convention pour dire « usage interne au module, ne pas l'utiliser de l'extérieur ».

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 module puis module.nom (forme par défaut) ; from module import nom pour quelques noms fréquents ; as pour un alias ; jamais import *.
  • 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 fonction main().
  • random : randint (bornes incluses), choice, choices, sample, shuffle (sur place), seed. Pour la sécurité : secrets.
  • math : sqrt, pi, floor, ceil, hypot, isclose… et statistics pour 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ère pathlib pour 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 Counter et regrouper avec defaultdict.
  • 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.

← Chapitre précédent | Sommaire | Chapitre suivant →