Programmation Python

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 — un module simple
# 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
Attributs d'un module
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

Toutes les formes 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 & dynamique
# 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.

Pattern __main__ — structure standard
# 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…
Exécuter un package comme script
# mon_package/__main__.py
# Permet : python -m mon_package

from . import main_function

if __name__ == "__main__":
    main_function()
Exemples : python -m
# 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

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.

mon_projet/
├── main.py point d'entrée
├── pyproject.toml config & dépendances
├── mon_package/ ← le package
│ ├── __init__.py rend le dossier package
│ ├── __main__.py python -m mon_package
│ ├── modele.py
│ ├── service.py
│ └── utils/ sous-package
│ ├── __init__.py
│ ├── validateurs.py
│ └── convertisseurs.py
└── tests/
├── __init__.py
└── test_modele.py
Importer depuis un package
# 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/__init__.py
"""
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.

Effet du __init__.py ci-dessus
# 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

Imports relatifs — syntaxe
# 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
SyntaxeSignification
from . import xPackage courant
from .module import xModule frère
from .sous import xSous-package
from .. import xPackage parent
from ..frere import xModule 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

__all__ — définir l'API publique
# 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.

Convention de nommage — visibilité
# 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.

pyproject.toml — template complet
[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"
Commandes pip essentielles
# 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

Éviter les conflits de noms
# ✗ 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 — détection et solution
# ✗ 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

ModuleUsageExemple clé
osSystème d'exploitation, fichiers, variables envos.environ, os.getcwd()
sysInterpréteur, arguments, sys.pathsys.argv, sys.exit()
pathlibChemins orientés objetPath("a") / "b"
datetimeDates et heuresdatetime.now(), timedelta
jsonSérialisation JSONjson.load(), json.dump()
csvFichiers CSVcsv.DictReader()
reExpressions régulièresre.match(), re.findall()
collectionsStructures spécialiséesCounter, defaultdict, deque
itertoolsItérateurs avancéschain, groupby, product
functoolsOutils fonctionnelslru_cache, wraps, partial
loggingJournalisationlogging.basicConfig(), logger.info()
argparseArguments en ligne de commandeArgumentParser.add_argument()
typingAnnotations de typeOptional, List, Dict, Union
abcClasses abstraitesABC, abstractmethod
threadingThreads (voir page dédiée)Thread, Lock, Queue
subprocessExécuter des commandes systèmesubprocess.run()
unittestTests unitaires (voir page dédiée)TestCase, assertEqual

Cheat sheet

Imports

import moduleModule entier
import module as mAlias
from module import xSymbole spécifique
from module import x as ySymbole + alias
from . import xImport relatif
from .. import xImport relatif parent

Structure package

__init__.pyCrée un package (peut être vide)
__main__.pypython -m mon_package
__all__ = [...]Contrôle import *
__version__Métadonnée de version
pip install -e .Install développement

Conventions de nommage

nomPublic
_nomConvention "protégé"
__nomName mangling (classe)
__nom__Dunder — réservé Python
NOMConstante (convention)

Débogage modules

module.__file__Chemin du fichier
module.__name__Nom du module
dir(module)Symboles disponibles
sys.modulesCache des modules importés
if __name__=="__main__"Code exécution directe