W3docs

Packages Python et système d'importation

Apprenez comment fonctionnent les packages Python : créez un package avec __init__.py, utilisez des imports absolus et relatifs, exposez une API publique claire et évitez les pièges courants.

Un package est un répertoire de modules Python que vous traitez comme une unité importable unique. Là où un module est un seul fichier .py, un package est un dossier — pouvant contenir de nombreux modules et sous-packages — que le système d'importation de Python peut parcourir comme un arbre. Ce chapitre explique comment créer des packages, contrôler ce qu'ils exposent, écrire des imports absolus et relatifs correctement, et éviter les pièges qui font trébucher les débutants.

Modules vs. Packages — la différence essentielle

Un module est un seul fichier .py :

greetings.py        ← module

Un package est un répertoire qui contient au moins un fichier spécial appelé __init__.py :

greetings/          ← package
    __init__.py
    english.py
    spanish.py

Les deux s'importent avec le même mot-clé import, mais un package vous offre une hiérarchie de nommage : greetings.english et greetings.spanish sont des modules distincts, mais ils partagent l'espace de noms greetings.

Quand utiliser un module plutôt qu'un package :

SituationUtiliser
Un utilitaire petit et autonomeModule (un seul fichier .py)
Plusieurs modules liés à regrouper sous un même nomPackage (un répertoire)
Une bibliothèque à distribuer sur PyPIPackage (avec la structure src/)

Le fichier __init__.py

__init__.py est ce qui fait d'un répertoire un package. Python l'exécute la première fois que le package (ou l'un de ses modules) est importé. Il peut être vide, ou il peut :

  • Importer des noms depuis des sous-modules pour les rendre disponibles au niveau du package
  • Exécuter une initialisation au niveau du package (configuration des logs, vérifications de version, etc.)
  • Définir __all__ pour contrôler from package import *

Structure minimale d'un package

myapp/
    __init__.py       ← can be empty
    utils.py
    config.py
# myapp/__init__.py  (empty — that is fine)
# myapp/utils.py
def greet(name):
    return f"Hello, {name}!"

Import depuis l'extérieur du package :

from myapp.utils import greet

print(greet("Alice"))   # Hello, Alice!

Exposer des noms au niveau du package

Un pattern courant consiste à importer les noms les plus utilisés dans __init__.py afin que les appelants puissent écrire from myapp import greet au lieu de from myapp.utils import greet.

# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIES

Les deux noms sont maintenant disponibles directement sur le package :

import myapp

print(myapp.greet("Bob"))   # Hello, Bob!
print(myapp.MAX_RETRIES)    # whatever config.py defines

Imports absolus

Un import absolu part toujours du package de niveau supérieur ou d'un répertoire figurant dans sys.path. Il ne dépend jamais de l'emplacement du fichier qui effectue l'import.

project/
    myapp/
        __init__.py
        utils.py
        services/
            __init__.py
            email.py

Dans email.py, un import absolu ressemble à ceci :

# myapp/services/email.py
from myapp.utils import greet   # absolute — starts from the top-level package

def send_welcome(user):
    message = greet(user)
    print(f"Sending: {message}")

Les imports absolus sont le style par défaut et recommandé (PEP 8). Ils sont sans ambiguïté quelle que soit la façon dont Python est invoqué.

Imports relatifs

Un import relatif utilise des points (.) pour naviguer dans l'arbre du package par rapport à l'emplacement du fichier courant.

  • . désigne le package courant
  • .. désigne le package parent
  • ... désigne le package grand-parent, et ainsi de suite
# myapp/services/email.py

# One dot — import from myapp.services (same directory)
from . import sms

# Two dots — import from myapp (parent directory)
from ..utils import greet

Quand utiliser les imports relatifs

Les imports relatifs sont utiles à l'intérieur d'un package quand vous voulez indiquer clairement que greet provient de ce package et non d'une bibliothèque externe portant le même nom. Ils facilitent aussi la refactorisation car les imports se déplacent avec le package si vous renommez le répertoire de niveau supérieur.

Piège : les imports relatifs ne fonctionnent qu'à l'intérieur d'un package. Si vous exécutez python myapp/utils.py directement, Python le traite comme un script autonome et non comme une partie d'un package, et un import relatif lève ImportError: attempted relative import with no known parent package. Exécutez le package avec python -m myapp.utils à la place.

# Wrong — runs utils.py as a script, breaking relative imports
$ python myapp/utils.py

# Right — runs utils.py as part of the myapp package
$ python -m myapp.utils

Contrôler l'API publique avec __all__

__all__ est une liste de noms que from package import * exporte. Elle documente aussi ce que le package considère comme public.

# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES

__all__ = ["greet", "MAX_RETRIES"]   # farewell is intentionally not exported

Désormais, from myapp import * n'importe que greet et MAX_RETRIES. La fonction farewell existe toujours ; elle ne fait simplement pas partie de l'interface publique annoncée. Les noms préfixés d'un seul underscore (_private) sont également exclus de import * par convention.

Packages imbriqués (sous-packages)

Les packages peuvent contenir d'autres packages. Chaque sous-répertoire a besoin de son propre __init__.py.

analytics/
    __init__.py
    reports/
        __init__.py
        daily.py
        weekly.py
    charts/
        __init__.py
        bar.py
        pie.py

Importez un module profondément imbriqué avec le chemin complet en notation pointée :

from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChart

Ou, si analytics/__init__.py les expose :

# analytics/__init__.py
from .reports.daily import generate_report
# caller
from analytics import generate_report

Jusqu'où aller dans l'imbrication ?

Un package de trois ou quatre niveaux de profondeur est généralement le signe qu'il est devenu trop grand et devrait être divisé en packages de niveau supérieur distincts (installables séparément). Pour la plupart des projets, deux niveaux (package.module) suffisent.

Exemple pratique : construire un package geometry

Construisons pas à pas un package petit mais réaliste.

Structure du répertoire

geometry/
    __init__.py
    shapes.py
    conversions.py

shapes.py

# geometry/shapes.py
import math

def circle_area(radius):
    """Return the area of a circle with the given radius."""
    if radius < 0:
        raise ValueError("radius must be non-negative")
    return math.pi * radius ** 2

def rectangle_area(width, height):
    """Return the area of a rectangle."""
    return width * height

def triangle_area(base, height):
    """Return the area of a triangle."""
    return 0.5 * base * height

conversions.py

# geometry/conversions.py

def degrees_to_radians(degrees):
    """Convert degrees to radians."""
    import math
    return degrees * math.pi / 180

def radians_to_degrees(radians):
    """Convert radians to degrees."""
    import math
    return radians * 180 / math.pi

__init__.py — exposer les noms clés

# geometry/__init__.py
"""
geometry — simple 2-D geometry utilities.

Public API:
    circle_area(radius) -> float
    rectangle_area(width, height) -> float
    triangle_area(base, height) -> float
    degrees_to_radians(degrees) -> float
    radians_to_degrees(radians) -> float
"""

from .shapes import circle_area, rectangle_area, triangle_area
from .conversions import degrees_to_radians, radians_to_degrees

__all__ = [
    "circle_area",
    "rectangle_area",
    "triangle_area",
    "degrees_to_radians",
    "radians_to_degrees",
]

Utilisation du package

# main.py (sits next to the geometry/ directory)
import geometry

print(geometry.circle_area(5))           # 78.53981633974483
print(geometry.rectangle_area(4, 6))     # 24
print(geometry.degrees_to_radians(90))   # 1.5707963267948966

Ou avec des imports sélectifs :

from geometry import circle_area, degrees_to_radians

print(circle_area(3))              # 28.274333882308138
print(degrees_to_radians(180))     # 3.141592653589793

Packages namespace (Python 3.3+)

Depuis Python 3.3, un répertoire sans __init__.py est un package namespace. Python fusionne tous les répertoires portant le même nom dans sys.path en un seul package logique. C'est principalement utile pour les grandes organisations qui répartissent un seul package sur plusieurs dépôts ou répertoires d'installation.

Pour le développement quotidien, incluez toujours __init__.py. Cela rend votre intention sans ambiguïté et fonctionne dans toutes les versions de Python.

Comment Python trouve les packages

Lorsque vous écrivez import geometry, Python parcourt sys.path dans l'ordre :

  1. Le répertoire du script en cours d'exécution (ou le répertoire courant en mode interactif)
  2. Les répertoires dans la variable d'environnement PYTHONPATH
  3. Les répertoires de la bibliothèque standard
  4. Le répertoire site-packages (où vivent les packages installés par pip)
import sys
print(sys.path)

Le répertoire du package doit se trouver directement dans l'un de ces emplacements. Si geometry/ est dans /home/alice/projects/, Python ne le trouvera pas sauf si /home/alice/projects/ est dans sys.path.

Conseil : utilisez un environnement virtuel et installez votre package en mode développement (pip install -e .) pour que Python le trouve toujours sans manipulation manuelle de sys.path.

Distribuer un package

Pour partager un package avec d'autres (ou l'installer avec pip), vous avez besoin d'un fichier pyproject.toml à la racine du projet :

my_project/
    pyproject.toml    ← build metadata
    src/
        geometry/
            __init__.py
            shapes.py
            conversions.py

Un pyproject.toml minimal :

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "geometry"
version = "0.1.0"
description = "Simple 2-D geometry utilities"
requires-python = ">=3.9"

Installez localement en mode éditable pendant le développement :

pip install -e .

Désormais, import geometry fonctionne n'importe où dans votre environnement virtuel, quel que soit votre répertoire courant.

Pièges courants

__init__.py manquant

Si vous oubliez __init__.py, Python 3 traite le répertoire comme un package namespace (ce qui fonctionne généralement quand même), mais Python 2 l'ignore complètement. Soyez explicite : ajoutez toujours __init__.py.

Nommer un package comme un module de la bibliothèque standard

Évitez les noms tels que math/, json/, os/, email/. Python pourrait importer votre package à la place de celui de la bibliothèque standard, cassant du code sans rapport.

Exécuter un module de package comme un script

Comme indiqué ci-dessus, exécuter python myapp/services/email.py directement casse les imports relatifs. Utilisez plutôt python -m myapp.services.email.

Imports circulaires entre modules du même package

Si shapes.py importe depuis conversions.py et que conversions.py importe depuis shapes.py, vous avez un import circulaire. Les symptômes incluent ImportError ou des noms qui apparaissent de façon inattendue comme None. La solution consiste généralement à déplacer la logique partagée dans un troisième module, ou à différer l'import à l'intérieur du corps d'une fonction.

# Delayed import — breaks the cycle at module load time
def some_function():
    from .shapes import circle_area   # imported only when the function is called
    ...

ImportError lors de l'utilisation d'imports relatifs hors d'un package

# Will raise: ImportError: attempted relative import with no known parent package
# if run as:  python myapp/utils.py

from . import config   # relative import inside utils.py

Exécutez-le avec python -m myapp.utils ou restructurez le code de sorte que le point d'entrée soit un script séparé qui importe le package.

Récapitulatif

ConceptEn bref
PackageUn répertoire avec __init__.py contenant des modules
__init__.pyFait d'un répertoire un package ; s'exécute au premier import
Import absolufrom myapp.utils import greet — toujours depuis la racine
Import relatiffrom ..utils import greet — relatif au fichier courant
__all__Liste les noms exportés par from package import *
Package namespaceRépertoire sans __init__.py ; Python 3.3+ uniquement
Installation éditablepip install -e . — package trouvable partout dans le venv

Consultez Python Modules pour l'organisation du code en fichier unique, Python pip pour l'installation de packages tiers, et Python Virtual Environments pour maintenir les dépendances de projet isolées.

Pratique

Pratique
Qu'est-ce qui fait d'un répertoire un package Python (dans les versions de Python antérieures à 3.3) ?
Qu'est-ce qui fait d'un répertoire un package Python (dans les versions de Python antérieures à 3.3) ?
Was this page helpful?