Cours de Python / Structurer un paquet et définir son point d’entrée avec __main__

Un paquet Python regroupe des modules liés dans une hiérarchie. Placer l’exécution de l’application dans une fonction main() appelée depuis un point d’entrée explicite permet d’importer ses fonctions sans démarrer automatiquement le programme.

Créer un paquet classique et choisir le dossier de lancement

Un paquet, également appelé package, peut organiser plusieurs fichiers Python. Cet exercice utilise un paquet classique contenant __init__.py ; les paquets d’espace de noms peuvent fonctionner sans ce fichier, mais suivent un autre modèle. Nous conservons ici une structure explicite et facile à vérifier.

Créez le dossier project, puis son sous-dossier expense_app et les trois fichiers suivants. Le terminal devra se trouver dans project, parent du paquet, lorsque vous exécuterez les commandes :

project/
  expense_app/
    __init__.py
    math_tools.py
    __main__.py

La leçon sur les modules et import explique les noms et la recherche de fichiers. Le présent exercice ajoute une hiérarchie de paquet et un point d’entrée __main__.py.

Enregistrer tous les fichiers et exécuter le paquet

Python 3 et sa bibliothèque standard suffisent ; aucun paquet tiers n’est requis. Enregistrez les trois fichiers en UTF-8. Le fichier expense_app/__init__.py contient :

"""Expense calculation package."""

Cette chaîne de documentation signifie « paquet de calcul des dépenses ». Elle ne déclenche aucun calcul. Le fichier expense_app/math_tools.py contient :

def total(amounts):
    return sum(amounts)

Le fichier expense_app/__main__.py contient :

from .math_tools import total

def main():
    print(total([3500, 8000, 2500]))

if __name__ == "__main__":
    main()

Avec l’environnement virtuel du projet activé, ouvrez le terminal dans project, et non dans expense_app, puis exécutez :

python -m expense_app

Sur macOS ou Ubuntu hors environnement virtuel, utilisez python3 -m expense_app si votre interpréteur se nomme python3. Le résultat doit être une ligne contenant 14000. L’option -m recherche le paquet comme un module et lance son __main__.py en fournissant le contexte du paquet.

La ligne from .math_tools import total est un import relatif : le point désigne le paquet courant. Exécuter le fichier par son chemin avec python expense_app/__main__.py n’établit pas le même contexte et provoque ici une ImportError liée à l’absence de paquet parent connu. Ne supprimez pas le point pour masquer la différence de mode d’exécution ; utilisez la commande prévue depuis project.

Comprendre __name__ et limiter les effets de bord

Le module exécuté comme programme principal reçoit "__main__" pour son nom __name__. Importé normalement, ce même fichier reçoit ici "expense_app.__main__". Le test if __name__ == "__main__": utilise cette distinction pour appeler main dans le premier cas seulement. Le nom main n’a pas, à lui seul, d’effet automatique : c’est l’appel explicite qui démarre le calcul.

Ce garde ne protège que les instructions placées dans son bloc. Les imports, définitions et autres instructions au niveau supérieur sont toujours traités pendant l’initialisation du module. Un affichage ou une opération sur un fichier laissé hors du garde pourrait donc se produire à l’import. Ici, main contient l’affichage et math_tools ne fait que définir une fonction.

La documentation de l’environnement principal et de __main__.py précise ce fonctionnement. Pour un paquet plus développé, un petit __main__.py peut aussi déléguer le travail à un autre module réutilisable. Le garde employé dans cet exercice permet en particulier de tester l’import explicite de ce module sans lancer main.

Ne confondez pas recherche d’un module et recherche d’un fichier de données. Une instruction comme open("data.csv") utilise normalement un chemin relatif au dossier de travail ; elle ne cherche pas automatiquement à côté du module importé. Si votre application ajoute des ressources, définissez des chemins explicites ou un mécanisme de ressources du paquet selon leur rôle.

Vérifier séparément exécution et réutilisation

Depuis le même dossier project, lancez la commande suivante :

python -c "import expense_app.__main__; print('imported')"

Elle doit afficher uniquement imported, qui signifie « importé ». Si le total 14000 apparaît aussi, vérifiez qu’un appel de main ou un calcul affiché n’est pas resté au niveau supérieur hors du garde. Ce contrôle complète le lancement par -m : un programme qui calcule correctement peut encore être gênant à réutiliser s’il s’exécute à l’import.

  1. Dans __main__.py, remplacez seulement 3500 par 4500. Le lancement python -m expense_app doit afficher 15000 ; la commande d’import doit toujours afficher seulement imported. Restaurez ensuite 3500.
  2. Sans modifier les fichiers, comparez depuis project le lancement par -m au lancement direct python expense_app/__main__.py. Le premier réussit et le second échoue sur l’import relatif.
  3. Si vous lancez python -m expense_app depuis l’intérieur d’expense_app dans cette structure non installée, son parent n’est plus automatiquement le dossier de recherche attendu. Revenez dans project et relancez les deux commandes de référence.

Si le paquet reste introuvable, vérifiez le dossier courant, les noms exacts des fichiers et le bon interpréteur. Renommer expense_app oblige aussi à modifier les chemins d’import et les commandes qui le désignent. Enfin, organiser ces sources n’équivaut pas à publier une distribution installable : cette autre étape exige notamment une configuration de projet, une construction et une vérification d’installation.