Leçon 05-4 — Modules, imports et projet multi-fichiers
1. Objectif
À la fin de cette leçon tu sauras : importer un module de la bibliothèque standard (import math) comme tes propres fichiers .py, choisir entre import module et from module import nom, protéger le code de test avec if __name__ == "__main__":, découper un programme en modules (un module = une responsabilité), et diagnostiquer un ModuleNotFoundError ou un import circulaire en moins de cinq minutes.
2. Pourquoi c'est important
Tous tes programmes tiennent aujourd'hui dans UN fichier. Ça ne durera pas : au-delà de ~150 lignes, un fichier unique devient une soupe où tout se cherche. Les modules sont la réponse de Python : chaque fichier .py EST un module, importable depuis un autre. C'est aussi la porte d'entrée vers la bibliothèque standard (des centaines de modules livrés avec Python : math, random, datetime, json…) — tu utilises from pathlib import Path depuis deux leçons sans qu'on ait expliqué cette ligne ; aujourd'hui, on l'explique. Enfin, savoir organiser un projet multi-fichiers est LA compétence qui sépare « des scripts » d'« un logiciel » — les niveaux 06 à 10 travaillent tous dans des projets à plusieurs fichiers.
3. Explication simple
Un module, c'est une boîte à outils : un fichier .py rempli de fonctions (et de variables). import va chercher la boîte ; le point . ouvre la boîte :
import math # va chercher la boîte "math"
print(math.sqrt(16.0)) # 4.0 — l'outil sqrt DE la boîte math
print(math.pi) # 3.141592653589793
Deuxième forme, from ... import : sortir UN outil de la boîte pour l'utiliser directement, sans préfixe :
from math import sqrt
print(sqrt(16.0)) # 4.0 — plus court, mais on ne sait plus d'où il vient
Et le point crucial : tes fichiers marchent pareil. Si tu as écrit geometry.py avec une fonction rectangle_area(), alors depuis un autre fichier du même dossier :
import geometry
print(geometry.rectangle_area(3, 4)) # 12
Le nom du module, c'est le nom du fichier sans le .py. Écrire un module, tu sais déjà le faire — c'est juste des fonctions dans un fichier.
4. Explication approfondie
a) Importer, c'est EXÉCUTER. import geometry ne « charge » pas passivement le fichier : Python exécute geometry.py de haut en bas, une fois, et garde le résultat (les fonctions définies, les variables créées). Conséquence directe : si le module contient des print() ou des input() au niveau du fichier, ils se déclenchent À L'IMPORT. C'est pourquoi un module bien élevé ne contient que des définitions… sauf, parfois, un bloc de test — voir (c).
b) Où Python cherche les modules. Dans l'ordre : (1) la bibliothèque standard, (2) le dossier du script lancé — pas le dossier courant du terminal, celui du .py que tu exécutes — et quelques autres endroits (⏩ les paquets installés avec pip, niveau 09). D'où la règle d'or des projets : tous les fichiers du projet dans un même dossier (la racine), et on lance toujours le script principal depuis cette racine. Fichiers de données et modules restent alors trouvables ensemble — souviens-toi de 05-2 : les chemins relatifs, EUX, partent bien du dossier courant du terminal. Les deux règles se rejoignent : lance depuis la racine.
c) if __name__ == "__main__": — le badge d'identité. Python donne à chaque fichier une variable automatique __name__. Sa valeur dépend de COMMENT le fichier est utilisé :
- lancé directement (
python geometry.py) →__name__vaut"__main__"; - importé (
import geometry) →__name__vaut"geometry".
D'où l'idiome, à la fin d'un module :
if __name__ == "__main__":
# ce bloc ne tourne QUE lancé directement, jamais à l'import
print(rectangle_area(3, 4)) # petits tests manuels du module
Double usage : (1) mettre des essais dans un module sans polluer ceux qui l'importent ; (2) marquer le point d'entrée du programme principal — l'idiome complet étant def main(): + if __name__ == "__main__": main(). Tu verras cette paire de lignes dans quasiment tout code Python réel.
d) import x ou from x import y ? Les deux sont corrects ; le choix est affaire de lisibilité :
import module+module.fonction(): on voit d'où vient chaque appel. À préférer quand on utilise plusieurs fonctions du module, ou que le nom seul serait ambigu (storage.load()est clair,load()ne l'est pas).from module import nom: bien pour un nom-vedette évident et fréquent (from pathlib import Path).from module import *: interdit — importe tout en vrac, on ne sait plus qui a défini quoi, et les noms s'écrasent entre eux.
e) Un module = une responsabilité. Le découpage type d'un petit programme à trois fichiers :
mon-projet/
main.py point d'entrée : assemble, boucle principale — le chef d'orchestre
storage.py tout ce qui touche au fichier de sauvegarde (load/save)
ui.py tout ce qui parle à l'humain (menus, saisies, affichages)
Le test du bon découpage : sauras-tu dire en un mot ce que fait chaque fichier ? Et la circulation va dans UN sens : main importe storage et ui ; storage et ui ne s'importent pas entre eux et n'importent jamais main. Quand deux modules s'importent mutuellement, Python tourne en rond — c'est l'import circulaire (section 7), et c'est presque toujours le signe qu'une fonction est dans le mauvais fichier.
Difficulté honnête : la mécanique d'import est simple, et le piège n'est pas où tu crois : c'est le découpage qui demande du métier — quoi va où, comment nommer. Tes premiers découpages seront discutables, c'est attendu ; celui de l'exercice moyen est guidé pour te donner un modèle. Et quand un import échoue, ne « bidouille » pas au hasard : la section 7 donne la checklist de diagnostic, elle couvre 95 % des cas.
5. Exemples commentés
Exemple 1 — la bibliothèque standard, enfin expliquée
import math
import random
from pathlib import Path # la ligne que tu écris depuis la leçon 05-2
print(math.sqrt(2.0)) # 1.4142135623730951
print(random.randint(1, 6)) # un dé : 1 à 6 (change à chaque lancement)
print(Path.cwd()) # pathlib est un module standard comme un autre
Convention : tous les imports en haut du fichier, un module par ligne. On repère ainsi d'un coup d'œil de quoi dépend un fichier.
Exemple 2 — ton premier module à toi
geometry.py :
"""Small geometry helpers."""
def rectangle_area(width, height):
return width * height
def rectangle_perimeter(width, height):
return 2 * (width + height)
main.py, dans le même dossier :
import geometry
print(geometry.rectangle_area(3, 4)) # 12
print(geometry.rectangle_perimeter(3, 4)) # 14
> python main.py
12
14
Rien de neuf dans geometry.py — des fonctions du niveau 04. La nouveauté tient en une ligne : import geometry rend TOUT le fichier disponible ici.
Exemple 3 — voir __name__ de ses propres yeux
badge.py :
print("badge.py exécuté, __name__ vaut :", __name__)
> python badge.py
badge.py exécuté, __name__ vaut : __main__
> python -c "import badge"
badge.py exécuté, __name__ vaut : badge
Le même fichier, deux identités : "__main__" en direct, son nom de module à l'import. Remarque au passage que l'import a EXÉCUTÉ le print — point (a) démontré.
Exemple 4 — le module avec ses tests embarqués
geometry.py, version complète :
"""Small geometry helpers."""
def rectangle_area(width, height):
return width * height
def rectangle_perimeter(width, height):
return 2 * (width + height)
if __name__ == "__main__":
# manual checks: run "python geometry.py" to execute them
print(rectangle_area(3, 4)) # expect 12
print(rectangle_perimeter(3, 4)) # expect 14
python geometry.py affiche les deux vérifications ; import geometry ne les affiche PAS. Le module est à la fois une boîte à outils silencieuse et son propre banc d'essai — c'est l'ancêtre artisanal des tests automatisés (⏩ pytest, niveau 08).
Exemple 5 — un vrai projet à trois modules
La liste de courses de 05-3, découpée. storage.py :
"""Load and save the shopping list (one item per line)."""
SAVE_FILE = "courses.txt"
def load_items():
try:
with open(SAVE_FILE, encoding="utf-8") as f:
items = []
for line in f:
items.append(line.strip())
return items
except FileNotFoundError:
return []
def save_items(items):
with open(SAVE_FILE, "w", encoding="utf-8") as f:
for item in items:
f.write(item + "\n")
ui.py :
"""Everything that talks to the user."""
def show_items(items):
if not items:
print("Liste vide.")
for i, item in enumerate(items, start=1):
print(f"{i}. {item}")
def ask_item():
return input("Article à ajouter : ").strip()
main.py :
"""Entry point: ties storage and ui together."""
import storage
import ui
def main():
items = storage.load_items()
while True:
choice = input("(a)jouter, (l)ister, (q)uitter : ").strip().lower()
if choice == "a":
items.append(ui.ask_item())
storage.save_items(items)
elif choice == "l":
ui.show_items(items)
elif choice == "q":
break
if __name__ == "__main__":
main()
Lis la circulation : main importe les deux autres ; storage et ui s'ignorent mutuellement. Chaque fichier répond à sa question — « où sont les données ? » → storage.py, « comment on parle à l'humain ? » → ui.py, « quel est le scénario ? » → main.py. Pour changer le format de sauvegarde, tu n'ouvres QU'un fichier.
Exemple 6 — importer sélectivement
from storage import load_items, save_items # les deux noms, sans préfixe
items = load_items()
save_items(items)
Équivalent au import storage de l'exemple 5, en plus court. Bon compromis dans main.py quand les noms sont clairs. Si deux modules exportaient chacun un load_items, on reviendrait au préfixe — le conflit de noms est la limite naturelle du from.
6. Erreurs fréquentes
1) Nommer son fichier comme un module standard
# tu as créé un fichier random.py, et ailleurs :
import random
print(random.randint(1, 6))
# AttributeError: module 'random' has no attribute 'randint'
Ton random.py fait de l'ombre au vrai module random : Python trouve LE TIEN d'abord (le dossier du script passe avant la bibliothèque standard). Symptômes déroutants garantis ; le message d'erreur des versions récentes suggère d'ailleurs de renommer. Ne nomme jamais un fichier random.py, math.py, json.py, test.py…
2) Du code exécutable au niveau du module
# storage.py — au niveau du fichier, hors de toute fonction :
items = load_items()
print(f"{len(items)} articles chargés")
Chaque import storage déclenche chargement et affichage — y compris quand l'importeur n'en voulait pas. Un module définit ; il n'AGIT qu'à la demande (dans ses fonctions) ou dans son bloc if __name__ == "__main__":.
3) Réimporter « pour recharger » dans la même session
Tu modifies geometry.py, tu refais import geometry dans le REPL… et rien ne change. Python n'importe qu'une fois par session et garde le module en cache ; les imports suivants sont ignorés. Solution simple : relancer le programme (ou le REPL). C'est aussi pour ça que l'exemple 5 peut importer storage depuis plusieurs fichiers sans le « ré-exécuter » à chaque fois.
4) L'import qui marche… grâce au hasard du dossier courant
> cd mon-projet
> python main.py ✅
> cd ..
> python mon-projet/main.py ✅ les IMPORTS marchent encore (dossier du script)...
💥 mais open("courses.txt") cherche ailleurs !
Piège subtil vu en (b) : les imports suivent le dossier DU SCRIPT, les chemins relatifs de fichiers suivent le dossier COURANT. Lancé « de loin », le programme importe bien ses modules mais lit/écrit ses fichiers au mauvais endroit — ou les recrée vides ! D'où LA règle simple : on lance depuis la racine du projet, toujours. (Le blindage Path(__file__).parent de 05-2 protège aussi les fichiers de données — les deux pratiques se complètent.)
5) L'import circulaire
# alpha.py # beta.py
from beta import from_beta from alpha import from_alpha
python alpha.py → ImportError: cannot import name 'from_beta' from 'beta' (traceback en boucle : alpha → beta → alpha). Python exécute alpha.py, tombe sur l'import de beta, commence beta.py… qui redemande alpha encore à moitié vide. Ce n'est pas une subtilité à contourner, c'est un défaut de découpage : la fonction partagée doit descendre dans un troisième module (helpers.py) que les deux importent — la circulation redevient à sens unique.
6) Modifier une copie du nom au lieu du module
from config import counter # counter est copié ICI au moment de l'import
counter = counter + 1 # ne modifie QUE la copie locale !
from x import y copie le nom dans ton fichier : réassigner cette copie ne touche pas la variable du module. Pour un état partagé, garde le préfixe (config.counter)… ou mieux : évite les variables partagées entre modules — passe les valeurs en paramètres, comme au niveau 04.
7. Lire les messages d'erreur
L'erreur signature de la leçon :
Traceback (most recent call last):
File "C:\Users\jtron\Claude\learnpython\lessons\level-05-fichiers-exceptions-modules\exercices\mes-reponses\main.py", line 1, in <module>
import storag
ModuleNotFoundError: No module named 'storag'
ModuleNotFoundError = « je n'ai trouvé ce module NULLE PART ». Checklist de diagnostic, dans l'ordre :
- Faute de frappe ? Compare lettre à lettre le nom importé et le nom du fichier (ici :
storagvsstorage.py). - Le fichier est-il dans le bon dossier ? Le module doit être à côté du script lancé (ou dans la bibliothèque standard).
- D'où as-tu lancé ?
pythonsans fichier (REPL) cherche dans le dossier courant : si tu as ouvert le REPL ailleurs, tes modules sont invisibles. - Le nom importé ne prend jamais le
.py:import storage, pasimport storage.py(ce dernier chercherait un modulepyDANS un paquetstorage…).
Sa cousine, quand le module existe mais pas le nom demandé :
ImportError: cannot import name 'load_item' from 'storage'
(C:\Users\jtron\Claude\learnpython\...\storage.py). Did you mean: 'load_items'?
Précieuse : elle donne le chemin du fichier réellement trouvé (vérifie que c'est le bon !) et suggère le nom probable. Enfin, l'import circulaire se reconnaît à son traceback en boucle — les mêmes fichiers qui se succèdent — et à son message :
ImportError: cannot import name 'from_beta' from 'beta'
(consider renaming '...beta.py' if it has the same name as a library you intended to import)
Deux causes possibles, que le message regroupe : soit le cercle alpha → beta → alpha (regarde le traceback : tes DEUX fichiers y alternent), soit l'erreur fréquente n°1 (ton fichier masque un module standard — d'où la suggestion de renommage).
8. Exercices — faciles
Règles : cette série se fait dans un sous-dossier par exercice (
mes-reponses/ex-05-4-a/, etc.), puisqu'on crée PLUSIEURS fichiers. Lance toujours depuis le dossier de l'exercice.
a) Crée temperature.py : deux fonctions, to_fahrenheit(celsius) et to_celsius(fahrenheit) (formules : c * 9 / 5 + 32 et (f - 32) * 5 / 9). Puis main.py qui l'importe (forme import temperature) et affiche la conversion de 20 °C et de 98.6 °F. Prédis les résultats avant de lancer.
b) Ajoute à temperature.py un bloc if __name__ == "__main__": affichant deux conversions de contrôle. Vérifie les DEUX comportements : python temperature.py (les contrôles s'affichent) puis python main.py (ils ne s'affichent PAS). Explique en commentaire pourquoi.
c) Réécris main.py avec la forme from temperature import to_fahrenheit, to_celsius. Puis provoque volontairement, et note en commentaire le message obtenu : (1) un import avec faute de frappe dans le nom du MODULE ; (2) un import d'une fonction qui n'existe pas dans le module. Deux erreurs différentes — lesquelles ?
9. Exercices — moyens
a) Découpe ton quiz ou ta liste de courses (exercice difficile de 05-3) en trois modules : storage.py (charger/sauver), ui.py (afficher/saisir), main.py (le scénario + if __name__ == "__main__":). Contraintes : aucun print/input dans storage.py, aucun open() hors de storage.py, aucun code exécutable au niveau des modules, et le comportement ne change pas d'un iota. (L'exemple 5 est ton modèle — mais ta version doit garder TOUTES tes fonctionnalités : retirer, valider…)
b) Le détective des imports. Sans exécuter, diagnostique ces trois pannes (chaque réponse : le message d'erreur attendu + la correction) :
# cas 1 : fichiers tools.py et main.py côte à côte ; main.py contient :
import tools.py
# cas 2 : main.py importe "helpers" ; le fichier s'appelle Helpers.py
# cas 3 : tu crées un fichier json.py pour t'entraîner, et plus loin :
import json
data = json.load(f)
Vérifie ensuite tes trois prédictions en reproduisant chaque cas dans un sous-dossier.
10. Exercices — difficiles
a) Répare un import circulaire. Crée d'abord la panne : orders.py a besoin de format_price() qui est dans customers.py, et customers.py a besoin de order_count() qui est dans orders.py — écris les deux fichiers avec un from ... import dans chaque, et un main.py qui importe orders ; constate l'ImportError. Puis répare SANS supprimer de fonctionnalité, en réorganisant les modules.
- Indice 1 : le problème n'est pas la syntaxe des imports, c'est qu'une fonction est mal rangée. Laquelle des deux n'a sa place ni dans
ordersni danscustomers? - Indice 2 : trois modules valent mieux que deux : que mettrais-tu dans un
shared.py(ouhelpers.py) ? - Indice 3 :
format_pricene dépend ni des commandes ni des clients → descends-la danshelpers.py, importé par les deux autres. La flèche des imports ne fait plus de boucle : dessine-la pour vérifier.
11. Mini-projet lié — « le gestionnaire de tâches passe en multi-fichiers »
Reprends ton gestionnaire de tâches (projects/mini-projects/06-gestionnaire-de-taches.md, v2 faite en 05-3) et réorganise-le en projet propre (~30–45 min) :
06-gestionnaire-de-taches/
main.py menu et boucle principale, if __name__ == "__main__"
storage.py load_tasks() / save_tasks(tasks) — seul à toucher au fichier
ui.py affichage du listing [x]/[ ], saisies validées
Critère de réussite : le comportement est identique à ta v2, et tu peux répondre en un mot à « où changerais-tu le format de sauvegarde ? », « où changerais-tu le texte du menu ? ». Même exercice possible sur le quiz interactif (load_questions → storage.py, ask_question → ui.py) — sa structure imposée par la fiche s'y prête déjà. Enfin, si tu as fait la simulation du script de rangement en 05-2, tu as maintenant tout pour terminer sa version --apply : gestion d'erreurs (05-3) + unique_destination en module réutilisable (05-4).
12. Correction / méthode de correction
Les solutions détaillées sont dans solutions/serie-05-4-solutions.md.
Méthode : (1) finis TOUS les exercices avant d'ouvrir les solutions ; (2) pour l'exercice de découpage, compare les FRONTIÈRES (qu'as-tu mis où, et pourquoi ?) plus que le code — un découpage différent et défendable est valable ; (3) vérifie le sens des flèches d'import dans tes projets (jamais de boucle, jamais vers main) ; (4) une ligne de journal par écart ; (5) refais de tête le lendemain.
13. À retenir
- Un fichier
.py= un module ;import fichier(sans.py) exécute le fichier UNE fois et donne accès à ses fonctions viamodule.fonction(). from module import nom: plus court, mais on perd la provenance ;import *: interdit.- Tous les imports en haut du fichier ; jamais de code exécutable au niveau d'un module (définitions seulement).
__name__vaut"__main__"lancé directement, le nom du module importé — d'où l'idiomeif __name__ == "__main__": main().- Un module = une responsabilité ; les imports circulent à sens unique (
main→ les autres, jamais l'inverse). - Import circulaire = fonction mal rangée → l'extraire dans un troisième module.
- On lance TOUJOURS depuis la racine du projet : imports ET fichiers de données restent cohérents.
ModuleNotFoundError: frappe → dossier → lieu de lancement → pas de.pydans l'import. Dans cet ordre.
14. Questions de révision
- Que se passe-t-il exactement, ligne par ligne, quand Python exécute
import geometrypour la première fois ? Et la deuxième fois ? - Quand préfères-tu
import storageàfrom storage import load_items, et inversement ? - Que vaut
__name__dans un fichier lancé directement ? Importé ? Et à quoi sert le blocif __name__ == "__main__":dans un module outil ? - Pourquoi appeler un fichier
random.pycasse-t-ilrandom.randint— par quel mécanisme précis ? - Ton programme multi-fichiers marche lancé depuis son dossier, mais lancé d'ailleurs il recrée un fichier de sauvegarde vide. Explique les DEUX règles de recherche différentes en jeu.
- À quoi reconnaît-on un import circulaire dans un traceback, et quelle est la réparation de fond ?
- Dans le découpage
main/storage/ui: où vivent lesinput()? Lesopen()? Pourquoi cette séparation paie-t-elle ? - Pourquoi
from config import counterpuiscounter += 1ne modifie-t-il pas la valeur vue par les autres modules ?
15. Checklist de compréhension
- Je sais créer un module et l'importer depuis un autre fichier, sans modèle.
- Je sais expliquer
if __name__ == "__main__":à quelqu'un d'autre, avec la démo des deux lancements. - Je sais choisir entre
import xetfrom x import yet justifier. - J'ai découpé un vrai programme en
main/storage/uiet il marche à l'identique. - Devant un
ModuleNotFoundError, je déroule la checklist au lieu de déplacer des fichiers au hasard. - J'ai provoqué ET réparé un import circulaire.
- Mes modules ne contiennent aucun code exécutable au niveau du fichier (hors bloc
__main__).
Si une case reste vide : refais les exercices de la section correspondante demain, AVANT de passer au niveau suivant.
16. Commit conseillé
git add lessons/level-05-fichiers-exceptions-modules/exercices/mes-reponses/
git commit -m "exercises: complete lesson 05-4 modules and multi-file projects"
Puis, si c'est ta fin de session : entrée de journal + git push. C'est aussi la fin du niveau 05 — relis les critères de sortie du README du niveau avant d'attaquer la POO.