Un projet Python qui démarre avec un seul fichier main.py finit souvent par exploser en dizaines de fichiers sans organisation claire. Le moment où on ne sait plus si la fonction parse_email vit dans utils.py, helpers.py ou tools.py, c’est le signe qu’on a raté le découpage en modules. Structurer son code en py module bien découpés dès le départ évite des semaines de refactoring six mois plus tard.
Arborescence d’un package Python : partir du cas d’usage, pas de la théorie
On voit souvent des tutoriels qui montrent une arborescence type avec src/, tests/, docs/ et un fichier __init__.py dans chaque répertoire. Le problème, c’est que cette structure ne dit rien sur la logique métier du projet.
A découvrir également : Code Pampers : mode d'emploi pour en profiter facilement
Prenons un cas concret : une application qui récupère des données utilisateur, les valide, puis envoie un email de confirmation. Trois responsabilités distinctes, trois modules possibles.
user/fetch.pycontient les fonctions qui interrogent la base ou l’API pour récupérer les données d’un useruser/validate.pyregroupe les règles de validation (format email, champs obligatoires, cohérence des data)notification/email.pygère l’envoi du message, le template, la connexion SMTP
Chaque fichier Python devient un module. Chaque répertoire contenant un __init__.py devient un package. Le découpage suit les responsabilités, pas les types d’objets (pas de models.py fourre-tout de 800 lignes).
A découvrir également : Comment optimiser le système de paie en entreprise ?
Le fichier __init__.py à la racine de chaque package sert à exposer l’interface publique. On y place les import qui simplifient l’usage depuis l’extérieur : from user import fetch_user plutôt que from user.fetch import fetch_user_from_db_v2.

Import circulaire entre modules Python : le piège classique
Sur un projet qui grossit, on finit presque toujours par tomber sur une ImportError liée à un import circulaire. Le module A importe le module B, qui importe le module A. Python lève une erreur ou, pire, importe un module partiellement initialisé sans prévenir.
La cause est rarement technique. C’est un problème de conception : deux modules qui dépendent mutuellement partagent probablement une responsabilité mal découpée.
Trois stratégies pour casser le cycle
La première consiste à extraire le code partagé dans un troisième module. Si user.py et email.py utilisent tous les deux une fonction format_address, on la déplace dans un module formatting.py que les deux importent sans dépendance croisée.
La deuxième approche : déplacer l’import à l’intérieur de la fonction qui en a besoin (import local). Ça fonctionne, mais c’est un pansement. On l’utilise quand le refactoring du module n’est pas possible immédiatement.
La troisième, souvent négligée, consiste à se demander si les deux fichiers ne devraient pas fusionner. Un module de 200 lignes bien organisé vaut mieux que deux modules de 100 lignes qui ne peuvent pas vivre l’un sans l’autre.
Fichier __init__.py et exposition de l’API publique d’un package
Le contenu du fichier __init__.py conditionne l’expérience de quiconque utilise le package. Un __init__.py vide oblige l’utilisateur à connaître la structure interne du répertoire pour importer quoi que ce soit. Un __init__.py surchargé de logique métier rend le package difficile à déboguer.
La bonne pratique tient en une règle : n’y mettre que des imports et éventuellement __all__. La variable __all__ définit explicitement ce qui est exporté quand quelqu’un écrit from package import *. Sans elle, Python exporte tout ce qui ne commence pas par un underscore, y compris des fonctions internes qu’on ne voulait pas rendre publiques.
Exemple pour un package math_utils :
__init__.py contient from .operations import add, multiply et __all__ = ["add", "multiply"]. Le fichier operations.py contient les fonctions def add, def multiply, mais aussi une fonction def _round_internal qui reste privée.
Cette approche permet de refactorer librement la structure interne (renommer un fichier, déplacer une fonction) sans casser le code qui dépend du package, parce que l’interface publique reste stable dans __init__.py.

Nommer et organiser ses modules pour un import lisible
Le nommage des modules a un impact direct sur la lisibilité du code qui les consomme. Quand on lit from data.cleanup import remove_duplicates, on comprend immédiatement ce que fait la fonction et d’où elle vient. Quand on lit from utils import func3, on ne comprend rien.
Quelques contraintes de nommage qui évitent les problèmes :
- Un nom de module en minuscules, sans tiret (underscore autorisé).
data_fetch.pyfonctionne,data-fetch.pyprovoque une erreur à l’import - Ne jamais nommer un module comme un module de la bibliothèque standard (
math.py,email.py,json.py). Python risque d’importer le fichier local au lieu du module standard, avec des erreurs incompréhensibles - Préférer des noms courts et spécifiques.
parse.pydit ce que fait le module.helpers.pyne dit rien - Un module qui dépasse 300-400 lignes mérite d’être scindé. Ce n’est pas une règle absolue (les retours varient sur ce point), mais au-delà, la navigation dans le fichier devient pénible
Pour les projets destinés à être publiés sur PyPI, le nom du package (le répertoire racine) doit être unique. Vérifier sur pypi.org avant de choisir un nom évite de devoir tout renommer au moment du setup.py ou du pyproject.toml.
Tester un module Python en isolation
Un module bien découpé se teste facilement. Si pour tester une fonction def validate_email, on doit instancier une connexion à la base de données, c’est que le module mélange validation et accès aux data.
La structure en modules séparés par responsabilité rend les tests unitaires directs. On importe la fonction, on lui passe des arguments, on vérifie le return. Pas besoin de mock complexe quand chaque module a une seule raison de changer.
Pour lancer les tests d’un seul module sans exécuter toute la suite, la commande python -m pytest tests/test_validate.py suffit. Le flag -m garantit que Python résout correctement les imports relatifs depuis la racine du projet, ce qui évite les ModuleNotFoundError qui découragent les développeurs juniors.
Le découpage en modules Python n’a rien de cosmétique. C’est ce qui détermine si un projet reste navigable quand l’équipe passe de deux à dix personnes, ou quand on revient sur le code après plusieurs mois. Un bon __init__.py, des noms de fichiers explicites et des imports sans cycle suffisent à garder un projet sous contrôle sans outillage supplémentaire.

