Un package en Python : regrouper plusieurs modules

Un package regroupe plusieurs modules Python dans un même dossier. Le mot désigne aussi ce que pip installe, et les deux noms coïncident rarement.
5 min de lecture
Believemy logo

Définition

Un projet Python qui avance finit par poser la même question : où ranger tout ce code sans que personne ne s'y perde ? Un seul fichier grossit vite, jusqu'à mélanger le calcul de TVA, la génération de PDF et la gestion des clients dans les mêmes lignes. Le package répond à cet ennui, en donnant un dossier à ce qui va ensemble. Un module est un fichier .py ; un package est le tiroir qui en contient plusieurs, et parfois d'autres tiroirs à l'intérieur. Voici un petit projet de facturation rangé de cette façon.

BASH
facturation/
    __init__.py
    tva.py
    pdf.py
    clients/
        __init__.py
        csv.py

L'import suit cette arborescence, en remplaçant les barres obliques par des points : ce que Python trouve sur le disque et ce que vous écrivez dans le code se correspondent trait pour trait.

Encore faut-il que Python reconnaisse ce dossier comme un package, et pas comme un simple tas de fichiers. C'est le rôle d'un fichier __init__.py, même vide : sa seule présence suffit à le rendre importable. À partir de là, l'instruction from descend dans l'arborescence comme dans un chemin de fichiers.

PYTHON
from facturation.tva import calculer
from facturation.clients.csv import lire

import facturation.pdf


Le mot désigne deux choses différentes

Un piège de vocabulaire guette presque tout le monde tôt ou tard. Le mot « package » sert à la fois pour le dossier que l'on importe et pour l'archive que l'on installe avec pip, et rien ne garantit que les deux portent le même nom.

Le mot « package »Ce qu'il désigne
Package d'importUn dossier de modules, ce qui suit le mot-clé import
Paquet distribuableUne archive publiée sur PyPI, ce que pip installe

Les deux noms coïncident souvent, jamais par obligation. On installe pillow et on importe PIL, on installe beautifulsoup4 et on importe bs4, on installe scikit-learn et on importe sklearn. Tant que cette double vie reste inconnue, une ModuleNotFoundError juste après une installation réussie semble absurde : la bibliothèque vient d'arriver, comment peut-elle être introuvable ? Elle est bien là, rangée sous un autre nom.


Ce que __init__.py sert vraiment à faire

Ce fichier vide laisse une question en suspens : à quoi sert-il, si on peut le laisser sans une ligne toute une carrière sans que rien ne casse ? Son utilité apparaît le jour où le package acquiert des utilisateurs, internes à l'équipe ou non : il devient alors la porte d'entrée, ce que l'extérieur a le droit de connaître du dossier.

PYTHON
# facturation/__init__.py
from .tva import calculer
from .pdf import generer

Les appelants écrivent alors from facturation import calculer sans savoir dans quel fichier interne habite la fonction. Le rangement du dossier redevient libre : déplacer calculer ailleurs, le lendemain, ne casse plus rien chez personne.

Attention

Beaucoup transforment ce fichier en poste de démarrage : lire une configuration, ouvrir une connexion à une base, appeler une API depuis __init__.py. Tout cela s'exécute au premier import, donc avant la moindre ligne utile, y compris pendant les tests et l'autocomplétion de l'éditeur. Un package qui met deux secondes à s'importer cache presque toujours ce genre de code au démarrage.


Un dossier ne vaut pas mieux qu'un fichier

Devant cette possibilité de tout ranger en dossiers, la tentation est de découper tôt, par précaution. C'est pourtant l'inverse qui coûte le moins cher : un fichier de trois cents lignes se lit très bien, alors que six fichiers de cinquante lignes qui s'importent entre eux se lisent mal, et finissent tôt ou tard en import circulaire. Le bon moment pour créer un package n'est pas celui où un fichier devient long, mais celui où il porte deux responsabilités qui n'évoluent plus au même rythme.


Le piège de l'import relatif

Une fois le code organisé en package, une surprise attend souvent au lancement. Un point devant un nom signifie « à côté de moi, dans le même package » : from .tva import calculer. Cette écriture fonctionne quand le module est chargé comme membre du package, et seulement dans ce cas : lancer le fichier directement prive Python de ce contexte, et l'import échoue avec une ImportError.

BASH
python facturation/pdf.py     # attempted relative import with no known parent package
python -m facturation.pdf     # fonctionne

La solution consiste à toujours lancer le fichier via le package qui le contient, avec l'option -m depuis le dossier parent, plutôt que de le lancer seul depuis l'intérieur. C'est exactement ce que fait, une fois le code installé, un point d'entrée déclaré au moment de l'empaquetage : personne n'a plus besoin d'y penser.


Questions fréquentes

Question

Le fichier __init__.py est-il encore obligatoire ?

Non depuis Python 3.3 : un dossier sans ce fichier reste importable, sous la forme d'un paquet d'espace de noms. Mieux vaut le créer quand même, car les outils d'empaquetage et de vérification de types s'appuient dessus, et un dossier oublié cesse alors de passer inaperçu au pire moment.

Question

L'installation a réussi, pourquoi l'import échoue-t-il ?

Presque toujours parce que l'installation et l'exécution n'ont pas eu lieu dans le même environnement virtuel. La seconde cause, plus fréquente qu'on ne le pense, est le nom : celui qui s'installe et celui qui s'importe diffèrent souvent, et seule la documentation du projet donne le bon.

Question

Faut-il publier un package pour le réutiliser ?

Non. Un dossier partagé entre deux projets s'installe très bien depuis un dépôt Git, avec un simple fichier pyproject.toml ou un outil comme poetry, et c'est la voie normale pour du code interne à une équipe. Publier sur PyPI ne sert qu'à rendre le code accessible à des inconnus, avec la documentation et le suivi de versions que cela suppose derrière.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.