Comment transformer un simple script en véritable py module ?

Un fichier .py qui fonctionne en local ne devient un py module réutilisable qu’à partir du moment où sa structure permet l’import sans effets de bord. La distinction paraît mince, mais elle conditionne tout : testabilité, distribution, intégration dans d’autres projets.

Le guard __name__ et la séparation import/exécution dans un py module

Le premier réflexe technique consiste à isoler le code exécutable derrière le guard if __name__ == "__main__". Sans lui, chaque import du fichier déclenche l’intégralité du script, y compris les appels à print, les connexions réseau ou les écritures disque.

A lire aussi : Pourquoi choisir du matériel informatique reconditionné en entreprise

Un module bien conçu expose des fonctions et des classes, rien d’autre. Le bloc __main__ sert uniquement de point d’entrée quand le fichier est appelé directement via python mon_module.py.

Nous recommandons de pousser cette logique plus loin en créant un fichier __main__.py séparé dans le package. Ce fichier appelle les fonctions du module, ce qui rend le code exécutable via python -m mon_package tout en gardant chaque module propre à l’import.

A découvrir également : Comment mettre CCleaner en français

Développeur Python créant la structure d'un module avec fichier __init__.py dans un espace de coworking

Arborescence de fichiers pour un package Python propre

Un script seul est un fichier. Un module, au sens de l’écosystème Python, est un répertoire structuré que pip sait installer. La différence se joue dans l’arborescence.

Voici la structure minimale que nous utilisons en production :

  • mon_package/ – répertoire racine contenant un fichier __init__.py (même vide, il signale à Python que le dossier est un package importable)
  • mon_package/core.py – le code métier, avec des fonctions à signature claire et des annotations de type
  • mon_package/__main__.py – point d’entrée CLI, qui importe depuis core.py
  • tests/ – répertoire de tests unitaires, miroir de la structure du package
  • pyproject.toml – fichier de configuration central du projet

Le fichier __init__.py contrôle ce qui est visible depuis l’extérieur. En y plaçant des imports explicites (from .core import ma_fonction), on définit l’API publique du module. Tout ce qui n’est pas importé dans __init__.py reste un détail d’implémentation.

Configurer pyproject.toml pour rendre le module installable

Le Python Packaging Authority (PyPA) recommande désormais pyproject.toml comme point d’entrée unique pour les nouveaux projets. L’approche setup.py est considérée comme obsolète pour la distribution de modules sur PyPI.

pyproject.toml remplace à la fois setup.py, setup.cfg et MANIFEST.in dans la plupart des cas. Un fichier minimal contient trois sections : [build-system] pour déclarer l’outil de build, [project] pour les métadonnées (nom, version, dépendances), et optionnellement [project.scripts] pour exposer des commandes CLI.

Le choix de l’outil de build (hatchling, flit, pdm, setuptools) se fait dans [build-system]. Hatchling et flit produisent des configurations plus courtes que setuptools pour un module simple. En revanche, setuptools reste pertinent pour les projets qui embarquent des extensions C.

Une fois le pyproject.toml en place, pip install -e . installe le module en mode développement. Chaque modification du code source est immédiatement reflétée sans réinstallation, ce qui élimine les problèmes de sys.path bidouillé à la main.

Architecture d'un module Python schématisée à la main dans un carnet de notes sur un bureau de bibliothèque universitaire

Typage statique et qualité de code dans un module Python

Un module destiné à être réutilisé devrait être typé. Les annotations de type ne sont pas un luxe académique : elles permettent aux IDE de proposer l’autocomplétion, aux outils comme mypy ou pyright de détecter les erreurs avant l’exécution, et aux utilisateurs du module de comprendre l’API sans lire l’implémentation.

Le typage se fait directement dans les signatures de fonctions :

def parse_config(filename: str, encoding: str = "utf-8") -> dict[str, str]:

Pour les cas plus complexes, le module typing fournit Optional, Union, TypeAlias et les génériques. Depuis Python 3.10, la syntaxe native (str | None) remplace avantageusement Optional[str].

Nous observons que les modules non typés posent systématiquement des problèmes d’intégration dans les projets qui utilisent un vérificateur statique. Le coût d’ajout du typage après coup est bien supérieur à celui de le faire dès la première version.

Gestion des imports relatifs et du path Python

Les erreurs de type ModuleNotFoundError viennent presque toujours d’une confusion entre imports relatifs et imports absolus. Un script lancé avec python fichier.py ne connaît que son propre répertoire dans sys.path. Un module installé via pip install -e . est référencé dans le site-packages, ce qui rend les imports absolus fiables partout.

À l’intérieur d’un package, les imports relatifs (from .core import ma_fonction) sont préférables. Ils rendent le module renommable sans casser les imports internes. Les imports absolus depuis la racine du package fonctionnent aussi, mais créent un couplage au nom du package.

Une erreur fréquente consiste à manipuler sys.path.insert(0, ...) dans le code pour forcer la résolution. Cette pratique crée des comportements imprévisibles selon le répertoire de travail. L’installation en mode éditable via pip élimine ce problème et devrait être le réflexe par défaut dès qu’un projet dépasse un seul fichier.

Documenter l’API publique du module avec des docstrings

Un module sans documentation est un module que personne ne réutilise. La convention Python repose sur les docstrings, placées juste après la définition d’une fonction, d’une classe ou du module lui-même.

Le format Google Style ou NumPy Style sont les deux standards de facto. Le choix importe peu, la cohérence oui. Un module qui mélange les deux styles perd en lisibilité.

  • Chaque fonction publique (présente dans __init__.py) reçoit une docstring décrivant ses paramètres, son retour et ses exceptions
  • Le fichier __init__.py lui-même contient une docstring de module qui décrit le rôle du package en deux ou trois phrases
  • Les fonctions internes préfixées par _ n’ont pas besoin de docstring exhaustive, mais un commentaire inline sur la logique non évidente reste utile

Les outils comme Sphinx ou pdoc génèrent automatiquement une documentation HTML à partir de ces docstrings. Le typage combiné aux docstrings produit une documentation de référence exploitable sans effort supplémentaire.

Le passage d’un script à un module Python tient en quelques décisions structurantes : séparer le code importable du code exécutable, poser une arborescence avec __init__.py, déclarer le projet dans pyproject.toml, typer les signatures et documenter l’API publique. Aucune de ces étapes n’est complexe isolément, mais c’est leur combinaison qui transforme un fichier jetable en brique logicielle fiable.