La question de savoir où placer chaque py module dans un projet Python n’est pas cosmétique. Un mauvais découpage génère des imports circulaires, des tests qui passent en local mais échouent à l’installation, et une base de code impossible à faire évoluer sans casser des dépendances transversales. Voici les décisions structurelles qui font la différence.
Layout src/ ou flat : un choix qui détermine la fiabilité des tests
La disposition src layout consiste à placer le package principal sous un répertoire src/ plutôt qu’à la racine du projet. Ce choix n’est pas qu’une convention esthétique.
Avec un layout flat, Python résout les imports en cherchant d’abord dans le répertoire courant. Un import monpackage depuis la racine trouvera le code source local même si certains fichiers sont absents du manifest de distribution. Les tests passent, mais l’utilisateur qui installe le wheel via pip peut se retrouver avec un package incomplet.
La disposition src/ force l’interpréteur à utiliser le package réellement installé (ou l’installation éditable) plutôt que le répertoire de travail. les tests reflètent ce que l’utilisateur reçoit, pas ce qui traîne sur le disque. La PyPA documente explicitement cet avantage dans son guide sur les layouts.
Un point souvent ignoré : même avec le src layout, une installation éditable via pip install -e . peut exposer des chemins de recherche différents d’une installation normale. Pour valider ce que le wheel contiendra, il est utile d’ajouter aux tests une installation du paquet construit, pas seulement du code source.
pyproject.toml comme point d’entrée unique de la configuration
setup.py est déprécié pour les nouveaux projets. pyproject.toml centralise les métadonnées du projet, la configuration du build backend (Hatchling, Flit, setuptools), et les dépendances d’installation.
Les dépendances déclarées dans la section [project.dependencies] de pyproject.toml décrivent ce qu’un utilisateur doit installer pour utiliser le package. Un fichier requirements.txt à la racine sert un objectif différent : épingler les versions exactes pour reproduire un environnement de développement ou de CI. les deux fichiers coexistent pour des usages distincts et ne font pas doublon.
Le fichier pyproject.toml peut aussi concentrer la configuration des outils (linters, formatters, mypy) via leurs sections dédiées, réduisant la prolifération de fichiers de configuration à la racine.

Découpage des modules Python : responsabilité unique, pas découpage mécanique
Le principe n’est pas de créer un fichier par classe ou par fonction, mais d’aligner les frontières de modules sur des axes de changement. Un module qui mélange la logique métier, l’accès à la base de données et la sérialisation JSON sera modifié pour trois raisons différentes, par trois personnes différentes, avec des risques de conflits constants.
Un découpage efficace suit quelques règles pratiques :
- Les modules de domaine contiennent la logique métier pure, sans dépendance vers des frameworks ou des couches d’infrastructure. Ils sont testables en isolation totale.
- Les modules d’infrastructure (accès base de données, appels HTTP, lecture de fichiers) s’isolent derrière des interfaces que les modules de domaine consomment via injection de dépendances. Un changement d’ORM ne touche pas la logique métier.
- Les modules utilitaires (
utils.py) sont un piège classique : ils deviennent des fourre-tout. Si unutils.pydépasse quelques fonctions génériques sans état, c’est le signe qu’une responsabilité mal identifiée s’y est accumulée. Découper par thème :text_processing.py,date_helpers.py.
Le rôle de __init__.py dans l’API publique d’un package
__init__.py n’est pas qu’un marqueur de package. Il définit ce que les consommateurs voient quand ils importent le package.
Un __init__.py vide expose la structure interne : l’utilisateur doit connaître from monpackage.core.processing import Processor. Un __init__.py qui réexporte les symboles publics permet from monpackage import Processor et découple l’API publique de l’organisation interne des fichiers.
contrôler les exports via `__all__` dans chaque module complète cette approche : cela documente l’interface prévue et évite qu’un from module import * pollue l’espace de noms avec des symboles internes.
La conséquence pratique : réorganiser les modules internes (renommer, déplacer des fichiers) ne casse pas le code des utilisateurs si l’API exposée dans __init__.py reste stable.
Organisation des tests par rapport aux modules Python
Deux conventions coexistent : placer les tests dans un répertoire tests/ à la racine (en miroir de la structure src/), ou intégrer les tests dans le package lui-même. La première approche est aujourd’hui dominante pour les projets distribués : les tests ne sont pas inclus dans le wheel et ne gonflent pas la taille du package installé.
La structure du répertoire tests/ doit refléter celle du package source. Un module src/monpackage/core/processing.py correspond à tests/core/test_processing.py. Cette symétrie simplifie la navigation et rend évidente l’absence de couverture.
Un point technique souvent négligé : le répertoire tests/ ne devrait pas être un package Python (pas de __init__.py). pytest et la plupart des outils de test modernes découvrent les fichiers de test sans marqueur de package, et l’absence de __init__.py évite des conflits de noms dans sys.modules.
Gestion de la configuration et des variables d’environnement
La configuration applicative (URLs de base de données, clés API, feature flags) ne doit jamais être dispersée dans les modules métier. Le pattern courant : un module config.py ou un package settings/ qui lit les variables d’environnement à un seul endroit, valide leur présence au démarrage, et expose des objets typés aux autres modules.
Les fichiers de configuration versionnés (.env.example, fichiers YAML de configuration par défaut) se placent à la racine ou dans un répertoire config/ dédié. les secrets ne se committent jamais, même dans un dépôt privé : un .env.example documente les variables attendues sans exposer les valeurs.
Cette séparation entre configuration et code a une conséquence directe sur la testabilité : les modules qui consomment la configuration via injection plutôt que par import direct sont testables avec des valeurs arbitraires sans manipuler les variables d’environnement du processus.

