Apprenez en lisant dans l'ordre

__init__.py et imports relatifs — Regrouper des fichiers en package

Apprends le rôle de __init__.py pour construire des packages Python et quand recourir aux imports absolus vs relatifs.

L'article précédent couvrait les modules sur un seul fichier et le fonctionnement de import. Une fois qu'une application grandit, tu voudras regrouper les modules liés sous un seul dossier en tant que package. Cet article parcourt comment importer des packages.

Un package est un dossier contenant __init__.py

Un package est un dossier qui contient un fichier spécial appelé __init__.py. Quand Python le trouve, le dossier entier devient un seul package et d'autre code peut le récupérer avec import nom_dossier. __init__.py est la première chose qui s'exécute quand le package est importé, et c'est un endroit courant pour déclarer quelles fonctions et classes le package expose.

Anatomie d'un package
my_package/ (un package)
  • __init__.py — le fichier qui s'exécute en premier quand le package est importé
  • calculation.py — un module (add / multiply)
  • string_utils.py — un module (format_name, etc.)
Le dossier my_package/ contient __init__.py, donc Python traite le dossier comme un package. Les modules internes comme calculation.py sont exposés à l'extérieur via __init__.py.

__init__.py peut être vide. Même un fichier vide suffit — Python a juste besoin de la présence du fichier pour reconnaître le dossier comme un package.

Importer sans passer par __init__.py

Même avec un __init__.py vide, tu peux toujours importer un module directement depuis le package. La forme est from nom_package.nom_module import nom_fonction — relie l'emplacement du fichier avec des points.

# my_package/__init__.py  <- vide, ça va

# my_package/calculation.py
def add(a, b):
    return a + b


# Dans main.py
from my_package.calculation import add   # précise le nom de fichier `calculation`
print(add(1, 2))   # 3

L'inconvénient : les appelants doivent savoir quel fichier contient quoi. Si tu réorganises le package plus tard (disons, scinder calculation.py en deux), chaque appelant doit changer aussi. Le motif de la prochaine section consistant à ré-exporter via __init__.py corrige ça — il permet aux appelants de référencer des noms courts directement attachés au package.

Regrouper l'API publique dans __init__.py

Écris from .nom_module import nom_fonction à l'intérieur de __init__.py et la fonction apparaît comme si elle vivait directement sous le package. Par exemple, from .calculation import add, multiply dans __init__.py permet aux appelants d'écrire from my_package import add, multiply — court et propre.

Ré-exporter via __init__.py pour des imports plus courts
my_package/main.py__init__.pyfrom .calculationimport add, multiplycalculation.pydef add()def multiply()from my_packageimport addadd(1, 2)appelableviaremonte
Quand __init__.py remonte add depuis calculation.py, les appelants peuvent écrire from my_package import add sans se soucier de quel fichier (calculation) le contient.

Le dossier my_package/ est attaché à gauche (ouvre calculation.py et __init__.py depuis 📂 Files pour inspecter).

① Utilise from my_package import add, multiply pour charger les deux fonctions.

② Affiche les résultats de add(10, 20) et multiply(3, 4).

Éditeur Python

Exécuter le code pour voir le résultat

Pourquoi regrouper l'API publique dans __init__.py

Si les appelants ne touchent qu'à ce que __init__.py expose, tu peux remanier les fichiers internes plus tard sans toucher au code des appelants. C'est de l'encapsulation classique au niveau du package — le motif de base du design de package.

from vs import — Deux façons de l'écrire

Il existe deux formes de import : from package import nom et import package. Les deux récupèrent la cible, mais la façon dont tu appelles ensuite est différente.

from vs import — Ce qui atterrit dans la portée
from my_pkg.calcimport adddans la portée :addappel :add(1, 2)importmy_pkg.calcdans la portée :my_pkgappel :my_pkg.calc.add(1, 2)
from package import nom fait entrer le nom lui-même dans la portée pour que tu puisses l'utiliser directement. import package ne fait entrer que le nom du package dans la portée ; les appels doivent utiliser le chemin complet pointé.
# Forme 1 : from ... import ...
from my_package.calculation import add
print(add(1, 2))   # add est directement utilisable


# Forme 2 : import ...
import my_package.calculation
print(my_package.calculation.add(1, 2))   # appel avec le chemin pointé complet


# Forme 2 + alias as (raccourcit les noms longs)
import my_package.calculation as calc
print(calc.add(1, 2))
FormeNom dans la portéeComment appeler
from my_package.calculation import addaddadd(1, 2)
import my_package.calculationmy_packagemy_package.calculation.add(1, 2)
import my_package.calculation as calccalccalc.add(1, 2)

La forme la plus courante est from package import nom — le nom de fonction est court et le code côté main se lit proprement. Mais quand deux packages différents exportent le même nom de fonction et que tu as besoin des deux, la forme import package garde le nom de module visible pour qu'il soit évident de savoir lequel tu appelles.

from vs import — Ce qui atterrit dans la portée de main
from my_pkg.calc import add
  • add — le nom lui-même atterrit dans la portée de main
  • Appel : add(1, 2) fonctionne directement
  • Court à écrire, mais ce n'est pas évident de quel package vient add
import my_pkg.calc
  • my_pkg — seul le nom du package atterrit dans la portée de main
  • Appel : my_pkg.calc.add(1, 2) avec le chemin pointé complet
  • Plus long à écrire, mais my_pkg.calc.add rend l'origine évidente
from package import nom fait entrer le nom lui-même dans la portée de main (= appels courts). import package ne fait entrer que le nom du package, donc les appels utilisent le chemin pointé complet (= l'origine est explicite).

Essaie les deux formes avec le package mathlib/ joint. calculator.py définit triple(n) (vérifie dans 📂 Files).

① Utilise la forme from ... import ... pour faire entrer triple, puis print(triple(7)).

② Ensuite utilise la forme import ... as ... pour faire aussi entrer mathlib.calculator sous l'alias calc, et print(calc.triple(7)).

Éditeur Python

Exécuter le code pour voir le résultat

Imports absolus vs relatifs

Et si des modules à l'intérieur d'un package devaient se référer entre eux ? Disons que utility/validator.py veut importer depuis utility/helper.py — il y a deux façons de l'écrire : import absolu et import relatif. Pour choisir entre les deux, tu dois d'abord être au clair sur ce que signifie la racine du projet.

Ce qu'est la racine du projet
project/ ← la racine (où se trouve main.py)
  • main.py ← le point d'entrée que tu lances en premier
  • config.py ← réglages à l'échelle de l'app
my_app/
  • __init__.py ← s'exécute quand import my_app se produit
utility/
  • __init__.py
  • validator.py
  • helper.py
La racine du projet est le dossier où se trouve main.py (le point d'entrée). Le my_app dans l'import absolu from my_app.utility import validator se réfère au dossier de même nom directement sous cette racine.

La racine du projet est le dossier qui contient le point d'entrée que tu lances avec python main.py. Quand tu écris l'import absolu from my_app.utility import validator, Python cherche my_app directement sous cette racine, puis descend dans utility/validator.py. Le chemin pointé reflète la structure de dossiers — c'est tout ce qu'est un import absolu.

TypeFormeCe que ça signifie
Import absolufrom my_app.utility import helperChemin complet depuis la racine du projet
Import relatiffrom . import helperRelatif au fichier dans lequel tu te trouves
Import relatif (parent)from .. import configCible un fichier un dossier plus haut
Deux façons de lire helper.py depuis validator.py
Absolufrom my_app.utilityimport helperChemin completdepuis la racineRelatiffrom . importhelperBasé surle dossier courant
Quand tu lis un module voisin dans le même package, l'absolu et le relatif fonctionnent tous les deux. Le . dans from . import signifie « le même dossier dans lequel je suis ».

La répartition habituelle : le point d'entrée (main.py) utilise des imports absolus pour atteindre les packages, et les modules à l'intérieur d'un package utilisent des imports relatifs pour se référer entre eux. Avec les imports relatifs, renommer un dossier de package plus tard ne force pas de changements dans le code interne.

# my_app/utility/validator.py
# Import relatif pour helper.py dans le même package
from .helper import log_message


def validate_user(user):
    if user.name and user.email:
        log_message("OK")
        return True
    log_message("problème détecté")
    return False


# my_app/utility/helper.py
def log_message(message):
    print("[LOG]", message)


# Pour atteindre config.py dans un autre package,
# remonte d'un cran avec .. :
# from ..config import get_config
Disposition de dossiers pour le code ci-dessus
my_app/
  • config.py ← cible de from ..config import ...
utility/
  • validator.py ← écrit from .helper import log_message
  • helper.py ← fournit log_message(...)
validator.py et helper.py sont côte à côte dans le même dossier utility/. C'est pour ça que from .helper import log_message dans validator.py utilise . — « ce dossier (= utility/) ». config.py est un niveau au-dessus dans my_app/, donc du point de vue de validator tu l'atteindrais comme ..config (.. = un niveau au-dessus).

Les points d'entrée ne peuvent pas utiliser d'imports relatifs

Un fichier que tu lances directement avec python main.py ne peut pas utiliser d'imports relatifs — tu vas toucher un ImportError. Les imports relatifs ne fonctionnent que quand le fichier est chargé en tant que membre d'un package. Depuis le point d'entrée, utilise toujours des imports absolus (par ex. from my_app.utility import validate_user).

Essaie les imports absolus et relatifs avec le package shop/ joint. Ouvre 📂 Files et tu verras que shop/__init__.py est vide — ni Cart ni to_yen n'est exposé à la racine du package pour l'instant.

① D'abord, lance main.py tel quel — tu obtiendras un ImportError parce que rien n'est exporté depuis shop.

② Ensuite, ouvre shop/__init__.py depuis 📂 et écris from .cart import Cart et from .formatter import to_yen en tant qu'imports relatifs (. = ce package), puis sauvegarde.

③ Ajoute l'utilisation de Cart à main.py — construis un Cart, ajoute apple à $100 et orange à $200, puis formate le total avec to_yen() et affiche-le. Le côté main utilise un import absolu, les internes du package utilisent des imports relatifs — c'est la répartition.

Éditeur Python

Exécuter le code pour voir le résultat

Une disposition style vrai projet

Avec ces règles, tu peux assembler des structures de dossiers qui correspondent à de vraies apps. Voici une disposition scindée en trois packages — settings, database et utility :

Une disposition proche d'un vrai projet
project/ (racine du projet)
  • main.py — point d'entrée (le fichier que tu lances avec python)
  • config.py — réglages à l'échelle de l'app
my_app/
  • __init__.py — API publique du package my_app
database/
  • __init__.py
  • connection.py / models.py
utility/
  • __init__.py
  • validator.py / helper.py
Le point d'entrée main.py récupère le package my_app/ entier ; les modules à l'intérieur utilisent des imports relatifs pour s'atteindre. Chaque sous-dossier a son propre __init__.py qui regroupe l'API publique de ce sous-package.

Depuis main.py tu fais des appels vers l'extérieur avec des imports absolus comme from my_app.utility import validate_user. À l'intérieur de utility/, validator.py atteint helper.py avec from .helper import log_message — un import relatif. C'est la répartition canonique du travail.

La frontière entre absolu et relatif
my_app/utility/main.pyvalidator.pyhelper.pyabsolurelatif
main.py atteint le package utility avec absolu (chemin depuis la racine) ; à l'intérieur d'utility, validator → helper passe en relatif (chemin depuis là où tu es). Connaître la frontière empêche les renommages de dossiers ultérieurs de se propager.

Implémente validator.py à l'intérieur du package my_app/utility/ toi-même et appelle-le depuis main.py (ouvre validator.py depuis 📂 Files, édite, puis sauvegarde avec Cmd+S ou 💾). helper.py est déjà fait.

① Implémente validate_user(email) dans validator.py :

- Commence par from .helper import log_message — un import relatif de helper.py dans le même dossier

- Si email contient à la fois @ et ., appelle log_message("OK: " + email) et retourne True

- Sinon appelle log_message("problème détecté: " + email) et retourne False

② Dans main.py, importe-le avec un import absolu (from my_app.utility import validate_user) et print(validate_user("alice@example.com")).

Éditeur Python

Exécuter le code pour voir le résultat

À partir d'ici, c'est avancé — n'hésite pas à revenir en arrière si tu bloques

Le prochain exercice est le point d'orgue — construire deux packages en parallèle. Tu seras le plus à l'aise si le motif de ré-export via __init__.py te paraît naturel et que la distinction entre import absolu et relatif est solide pour toi. Si tu bloques, reviens à l'exercice précédent sur validator.py ou au schéma « Une disposition style vrai projet » ci-dessus pour te remettre en selle avant d'attaquer celui-ci.

Un challenge multi-dossiers

Il est temps d'essayer de construire deux packages en parallèle avec tout ce que tu as appris. Tu vas gérer un package produit catalog/ et un package billing/ dans des dossiers séparés, puis les orchestrer depuis main.py pour finaliser une commande. Répartir les responsabilités entre dossiers signifie que les changements aux données produit ne touchent que catalog/, et les changements au formatage de facture ne touchent que billing/ — la zone d'impact reste à l'intérieur d'un seul dossier.

Disposition du projet (catalog et billing en deux packages)
project/ ← racine du projet
  • main.py — point d'entrée (utilise les deux packages pour traiter une commande)
catalog/
  • __init__.py — ré-exporte from .products import get_price
  • products.py — implémente get_price(name)
billing/
  • __init__.py — ré-exporte from .invoice import format_invoice
  • invoice.py — implémente format_invoice(name, qty, unit_price)
catalog/ contient les données produit (products.py) ; billing/ contient le formatage de facture (invoice.py). Chaque __init__.py ré-exporte l'API publique, donc main.py peut faire des appels vers l'extérieur avec deux imports absolus : from catalog import get_price et from billing import format_invoice.
main.py orchestre deux packages
catalogget_price()main.pyorchestre les deuxbillingformat_invoice()from catalog import get_pricefrom billing import ...
main.py fait des appels vers l'extérieur avec des imports absolus pour récupérer unit_price depuis catalog et formater la facture via billing. Chaque package utilise en interne des imports relatifs dans __init__.py pour ré-exporter .products / .invoice.

Complète les packages catalog/ et billing/ joints y compris leurs fichiers __init__.py, puis orchestre-les depuis main.py avec des imports absolus (ouvre chaque fichier depuis 📂 Files et sauvegarde avec Cmd+S ou 💾).

① Dans catalog/products.py, implémente get_price(name)"apple" → 100, "orange" → 150, sinon 0 (un dict + dict.get(name, 0) est pratique).

② Dans catalog/__init__.py, écris from .products import get_price pour que l'extérieur puisse appeler from catalog import get_price (un import relatif).

③ Dans billing/invoice.py, implémente format_invoice(name, qty, unit_price) — calcule qty * unit_price et retourne une chaîne comme "apple x 3 = $300".

④ Dans billing/__init__.py, écris from .invoice import format_invoice.

⑤ Dans main.py, fais from catalog import get_price et from billing import format_invoice (imports absolus). Commande 3 de "apple", cherche le prix unitaire, formate la facture, et print().

Éditeur Python

Exécuter le code pour voir le résultat

À partir d'ici, c'est du bonus — rare dans le code réel

La section ci-dessous couvre __all__ et est purement informative. Le code du monde réel n'utilise presque jamais from package import *, donc cette partie n'affecte pas le fil principal. Tant que tu maîtrises la forme import avec noms explicites, n'hésite pas à survoler cette section.

Contrôler from package import * avec __all__

Quand quelqu'un écrit from my_package import * pour tout récupérer avec une étoile, tu peux contrôler ce qui est exposé via __all__ dans __init__.py. Avec __all__ = ["add"], l'import * ne récupère que add — rien d'autre.

__all__ restreint ce qui est exposé
__init__.pyfrom .calc importadd, multiply__all__ = ["add"]from pkgimport *add→ récupérémultiply→ pas récupéré(NameError)
Avec __all__ = ["add"] dans __init__.py, seul add passe via from package import *. multiply est laissé de côté (tu peux toujours l'attraper explicitement).
# my_package/__init__.py
from .calculation import add, multiply

__all__ = ["add"]   # seul add est exposé via *

# Côté appelant
# from my_package import *
# add(1, 2)        # OK
# multiply(1, 2)   # NameError (pas récupéré par *)

En pratique, évite les imports *

from my_package import * est dur à lire — tu ne peux pas dire en un coup d'œil ce qui est entré. Le code du monde réel utilise presque toujours des noms explicites comme from my_package import add, multiply. Considère __all__ comme un *filet de sécurité pour le cas rare où quelqu'un utilise ``**.

Vérifie le comportement de __all__ avec le package bundle/ joint. Ouvre bundle/__init__.py depuis 📂 Files — il fait entrer à la fois add et multiply mais restreint l'exposition avec __all__ = ["add"].

① Dans main.py, fais from bundle import * et affiche add(2, 3).

② Ensuite enveloppe un appel à multiply(2, 3) dans try/except et vérifie qu'il lève NameError (cet exercice utilise le runtime Pyodide compatible CPython — le premier chargement prend 5–15 secondes).

Éditeur Python

Exécuter le code pour voir le résultat

Cet article a couvert l'utilisation de __init__.py pour regrouper plusieurs modules en un seul package, la différence entre from package import nom et import package, le choix entre imports absolus et relatifs, et une disposition de dossiers proche d'un vrai projet.

QUIZ

Vérification des connaissances

Répondez à chaque question une par une.

Question 1Quel fichier dois-tu placer dans un dossier pour qu'il soit reconnu comme un package ?

Question 2Depuis my_app/utility/validator.py, quel est le bon import relatif pour charger helper.py dans le même dossier utility ?

Question 3Que se passe-t-il si tu écris un import relatif (from . import xxx) à l'intérieur d'un point d'entrée (un fichier que tu lances directement avec python main.py) ?