Modules
& Packages
Organiser, réutiliser et distribuer du code Python — de l'import simple au package installable.
Qu'est-ce qu'un module ?
Un module est un fichier .py contenant des définitions (fonctions, classes, variables) réutilisables. Importer un module exécute son code une seule fois — les imports suivants utilisent le cache.
# geometrie.py
"""Module de calculs géométriques."""
import math
PI = math.pi # constante publique
_PRECISION = 6 # _ = convention "privé" (non exporté)
def aire_cercle(rayon: float) -> float:
"""Calcule l'aire d'un cercle."""
return round(PI * rayon ** 2, _PRECISION)
def perimetre_cercle(rayon: float) -> float:
return round(2 * PI * rayon, _PRECISION)
class Rectangle:
def __init__(self, l, h): self.l, self.h = l, h
def aire(self): return self.l * self.h
import geometrie
# Attributs automatiques
print(geometrie.__name__) # "geometrie"
print(geometrie.__file__) # chemin absolu du .py
print(geometrie.__doc__) # docstring du module
# Lister les symboles publics
print(dir(geometrie))
# ['PI', 'Rectangle', 'aire_cercle', 'perimetre_cercle', …]
# Les _ sont inclus mais marqués comme privés par convention
# Vérifier si un module est déjà importé (cache)
import sys
print("geometrie" in sys.modules) # True
Python met les modules en cache dans sys.modules après le premier import. Les imports suivants de ce module sont quasi-instantanés — le code n'est pas réexécuté.
Syntaxes d'import
# 1. Import du module entier (recommandé)
import math
print(math.pi) # namespace clair
# 2. Alias (noms longs ou conventions)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 3. Import de symboles spécifiques
from math import sqrt, pi, cos
print(sqrt(16)) # sans préfixe
# 4. Import avec alias
from datetime import datetime as dt
# 5. ✗ Import étoile — DÉCONSEILLÉ
from math import * # pollue le namespace
# Seul cas acceptable : REPL interactif ou __init__.py réexportant
# Import conditionnel (compatibilité)
try:
import ujson as json # rapide si disponible
except ImportError:
import json # fallback stdlib
# Import paresseux (éviter les imports circulaires)
def traiter_image(chemin):
from PIL import Image # importé seulement si la fonction est appelée
return Image.open(chemin)
# Import dynamique (importlib)
import importlib
module = importlib.import_module("json")
data = module.loads('{"clé": 42}')
Placer tous les imports en haut du fichier (PEP 8). Les imports à l'intérieur de fonctions sont acceptés pour l'import paresseux ou pour éviter les imports circulaires — pas comme habitude générale.
__name__ & __main__
Quand Python exécute un fichier directement, __name__ vaut "__main__". Quand il est importé comme module, __name__ vaut le nom du fichier. Cette distinction permet d'avoir du code qui s'exécute seulement en lancement direct.
# calculatrice.py
def additionner(a: float, b: float) -> float:
return a + b
def diviser(a: float, b: float) -> float:
if b == 0:
raise ValueError("Division par zéro")
return a / b
# Exécuté seulement si : python calculatrice.py
# PAS exécuté si : import calculatrice
if __name__ == "__main__":
print(additionner(3, 4))
print(diviser(10, 2))
# Tests rapides, démo, CLI…
# mon_package/__main__.py
# Permet : python -m mon_package
from . import main_function
if __name__ == "__main__":
main_function()
# Lancer le serveur HTTP de la stdlib
python -m http.server 8080
# Lancer les tests
python -m pytest
# Installer une dépendance
python -m pip install requests
# Profiler un script
python -m cProfile mon_script.py
Chemin de recherche des modules
import sys
# Python cherche les modules dans cet ordre :
for chemin in sys.path:
print(chemin)
# 1. "" ou le répertoire du script courant
# 2. PYTHONPATH (variable d'environnement)
# 3. Répertoires d'installation (stdlib)
# 4. site-packages (packages installés via pip)
# Ajouter un chemin dynamiquement
sys.path.insert(0, "/mon/chemin/custom")
# Meilleure approche : variable d'environnement
# export PYTHONPATH="/mon/chemin:$PYTHONPATH"
Ne jamais nommer un fichier comme un module de la stdlib (math.py, os.py, json.py…). Python trouverait le vôtre en premier et les imports de la stdlib planteraient.
import requests
print(requests.__file__)
# /home/user/.venv/lib/python3.11/
# site-packages/requests/__init__.py
# Ou en ligne de commande
python -c "import requests; print(requests.__file__)"
Structure d'un package
Un package est un dossier contenant un fichier __init__.py (peut être vide). Il permet d'organiser un projet en modules hiérarchiques.
# Depuis main.py
# Import absolu (toujours préféré)
from mon_package.modele import Utilisateur
from mon_package.utils.validateurs import valider_email
import mon_package.service
# Utilisation
u = Utilisateur("Alice", "alice@heh.be")
ok = valider_email(u.email)
mon_package.service.traiter(u)
Toujours travailler avec des imports absolus (depuis la racine du projet) plutôt que relatifs dans les scripts principaux. Les imports relatifs (from . import) sont réservés à l'intérieur du package.
__init__.py — contrôle de l'API publique
"""
mon_package — description du package.
Version : 1.0.0
"""
# Réexporter les symboles principaux
# Permet : from mon_package import Utilisateur
# au lieu de : from mon_package.modele import Utilisateur
from .modele import Utilisateur
from .service import traiter, envoyer
# Métadonnées
__version__ = "1.0.0"
__author__ = "Erwin"
# Contrôle de import * (optionnel)
__all__ = ["Utilisateur", "traiter", "envoyer"]
Un __init__.py vide suffit à créer un package. On y ajoute des réexportations pour simplifier l'API publique — l'utilisateur importe depuis mon_package sans connaître la structure interne.
# Sans __init__.py réexportant :
from mon_package.modele import Utilisateur # long
# Avec __init__.py réexportant :
from mon_package import Utilisateur # simple
import mon_package
print(mon_package.__version__) # "1.0.0"
Imports relatifs
# Dans mon_package/service.py
# . = package courant (mon_package)
from . import modele
from .modele import Utilisateur
# .utils = sous-package utils
from .utils import validateurs
from .utils.validateurs import valider_email
# .. = package parent
# (depuis mon_package/utils/convertisseurs.py)
from .. import modele # remonte à mon_package
from ..service import traiter
| Syntaxe | Signification |
|---|---|
from . import x | Package courant |
from .module import x | Module frère |
from .sous import x | Sous-package |
from .. import x | Package parent |
from ..frere import x | Module oncle |
Les imports relatifs ne fonctionnent pas si le fichier est exécuté directement (python service.py). Ils nécessitent d'être dans un contexte de package. Utiliser python -m mon_package.service à la place.
__all__ — contrôle des exports
# validateurs.py
__all__ = ["valider_email", "valider_telephone"]
# Seules ces fonctions sont exposées avec import *
# Les autres restent accessibles mais "privées"
def valider_email(email: str) -> bool:
import re
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
def valider_telephone(tel: str) -> bool:
return tel.replace(" ", "").isdigit()
def _nettoyer_chaine(s: str) -> str: # "privé"
return s.strip().lower()
# from validateurs import *
# → importe uniquement valider_email et valider_telephone
__all__ sert aussi de documentation — en lisant __all__, on comprend immédiatement quels symboles font partie de l'API publique du module.
# Public — utilisable depuis l'extérieur
def ma_fonction(): ...
class MaClasse: ...
CONSTANTE = 42
# "Protégé" — convention : ne pas utiliser hors du module
def _helper(): ...
class _BaseInterne: ...
# "Privé" Python — name mangling dans les classes
class MaClasse:
def __init__(self):
self.__secret = 42 # renommé _MaClasse__secret
pyproject.toml
Le fichier pyproject.toml est le standard moderne pour décrire un projet Python — il remplace setup.py et setup.cfg.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mon-package"
version = "1.0.0"
description = "Description courte"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
{name = "Erwin", email = "erwin.desmet@heh.be"}
]
# Dépendances obligatoires
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
# Dépendances optionnelles
[project.optional-dependencies]
dev = ["pytest", "black", "ruff"]
# Point d'entrée CLI
[project.scripts]
mon-outil = "mon_package.__main__:main"
# Installer le projet en mode développement
# (les modifications sont prises en compte immédiatement)
pip install -e .
# Installer avec les dépendances dev
pip install -e ".[dev]"
# Construire le package distribuable
python -m build
# → dist/mon_package-1.0.0.tar.gz
# → dist/mon_package-1.0.0-py3-none-any.whl
# Publier sur PyPI
python -m twine upload dist/*
pip install -e . (editable install) est indispensable pendant le développement. Les changements dans le code source sont immédiatement visibles sans réinstaller le package.
Namespaces & conflits
# ✗ Conflit — deux sqrt dans le namespace
from math import sqrt
from cmath import sqrt # écrase le précédent !
# ✓ Utiliser des alias
from math import sqrt as sqrt_reel
from cmath import sqrt as sqrt_complexe
# ✓ Ou garder le module comme namespace
import math
import cmath
math.sqrt(4) # 2.0
cmath.sqrt(-1) # 1j
# ✗ Import circulaire — ERREUR
# a.py
from b import func_b # b importe a → ImportError
# b.py
from a import func_a # a importe b → cycle
# ✓ Solutions :
# 1. Restructurer : extraire le code partagé dans un 3e module
# 2. Import paresseux dans la fonction
def func_a():
from b import func_b # importé au moment de l'appel
func_b()
# 3. Annotation de type en string (TYPE_CHECKING)
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import TypeB # uniquement pour le type checker
Modules stdlib incontournables
| Module | Usage | Exemple clé |
|---|---|---|
os | Système d'exploitation, fichiers, variables env | os.environ, os.getcwd() |
sys | Interpréteur, arguments, sys.path | sys.argv, sys.exit() |
pathlib | Chemins orientés objet | Path("a") / "b" |
datetime | Dates et heures | datetime.now(), timedelta |
json | Sérialisation JSON | json.load(), json.dump() |
csv | Fichiers CSV | csv.DictReader() |
re | Expressions régulières | re.match(), re.findall() |
collections | Structures spécialisées | Counter, defaultdict, deque |
itertools | Itérateurs avancés | chain, groupby, product |
functools | Outils fonctionnels | lru_cache, wraps, partial |
logging | Journalisation | logging.basicConfig(), logger.info() |
argparse | Arguments en ligne de commande | ArgumentParser.add_argument() |
typing | Annotations de type | Optional, List, Dict, Union |
abc | Classes abstraites | ABC, abstractmethod |
threading | Threads (voir page dédiée) | Thread, Lock, Queue |
subprocess | Exécuter des commandes système | subprocess.run() |
unittest | Tests unitaires (voir page dédiée) | TestCase, assertEqual |
Cheat sheet
Imports
| import module | Module entier |
| import module as m | Alias |
| from module import x | Symbole spécifique |
| from module import x as y | Symbole + alias |
| from . import x | Import relatif |
| from .. import x | Import relatif parent |
Structure package
| __init__.py | Crée un package (peut être vide) |
| __main__.py | python -m mon_package |
| __all__ = [...] | Contrôle import * |
| __version__ | Métadonnée de version |
| pip install -e . | Install développement |
Conventions de nommage
| nom | Public |
| _nom | Convention "protégé" |
| __nom | Name mangling (classe) |
| __nom__ | Dunder — réservé Python |
| NOM | Constante (convention) |
Débogage modules
| module.__file__ | Chemin du fichier |
| module.__name__ | Nom du module |
| dir(module) | Symboles disponibles |
| sys.modules | Cache des modules importés |
| if __name__=="__main__" | Code exécution directe |