iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Python · Débutant

Python

Apprendre Python de zéro jusqu'au niveau expert : syntaxe, structures de données, POO, générateurs, décorateurs, typage, tests, async et internals du langage.

Python 3.13uvpytestmypyasyncio
01Introduction et installation02Variables et types de base03Conditions et boucles04Fonctions05Listes, tuples, dictionnaires et ensembles06Modules, fichiers et exceptions07Programmation orientée objet08Itérateurs, générateurs, décorateurs et context managers09Typage statique et tests10Concurrence : threads, processus et asyncio11Niveau expert : internals, métaprogrammation et performance
Chapitre 9·45 min

Typage statique et tests

Un script de 50 lignes survit sans types ni tests. Une application de 20 000 lignes maintenue à plusieurs, non. Python reste dynamique à l'exécution, mais son écosystème d'outils permet d'obtenir des garanties proches d'un langage typé.

Les annotations de type

def moyenne(notes: list[float]) -> float:
    return sum(notes) / len(notes)
 
nom: str = "Ada"
cache: dict[str, int] = {}

L'interpréteur ignore ces annotations. C'est un vérificateur externe (mypy, Pyright) qui les exploite.

Les types utiles

from typing import Any, Literal, TypedDict, Callable, Iterator
from collections.abc import Sequence, Mapping
 
def trouver(id: int) -> User | None: ...            # optionnel
def niveau(n: Literal["debug", "info", "error"]): ...
def appliquer(f: Callable[[int], str], x: int) -> str: ...
def compter(lignes: Iterator[str]) -> int: ...
 
class UserJSON(TypedDict):
    id: int
    email: str
    admin: bool

Acceptez des types larges en entrée (Sequence, Mapping), renvoyez des types précis (list, dict). Une fonction qui prend Sequence[int] accepte une liste comme un tuple.

Génériques

Depuis Python 3.12, la syntaxe des génériques est native :

def premier[T](elements: Sequence[T]) -> T:
    return elements[0]
 
class Pile[T]:
    def __init__(self) -> None:
        self._items: list[T] = []
 
    def empiler(self, item: T) -> None:
        self._items.append(item)
 
    def depiler(self) -> T:
        return self._items.pop()
 
p = Pile[int]()
p.empiler("a")   # erreur signalée par mypy

Et les alias :

type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None

mypy en pratique

uv add --dev mypy
uv run mypy src --strict
pyproject.toml
[tool.mypy]
python_version = "3.13"
strict = true
warn_unreachable = true

Sur un projet existant, commencez sans strict, puis durcissez module par module. Activer --strict sur 30 000 lignes non typées produit des milliers d'erreurs et décourage tout le monde.

Validation à l'exécution avec Pydantic

Les annotations ne protègent pas contre les données venues de l'extérieur (API, formulaire, fichier). Pydantic les utilise pour valider réellement :

uv add "pydantic[email]"
from pydantic import BaseModel, EmailStr, Field
 
class Inscription(BaseModel):
    email: EmailStr
    age: int = Field(ge=13, le=120)
    newsletter: bool = False
 
Inscription.model_validate({"email": "ada@example.com", "age": "36"})
# age converti en int 36
 
Inscription.model_validate({"email": "pas-un-email", "age": 5})
# ValidationError avec le détail des deux champs

FastAPI repose entièrement sur ce mécanisme.

Tests avec pytest

uv add --dev pytest pytest-cov

Un test est une fonction préfixée test_ qui utilise assert :

src/boutique/prix.py
def appliquer_remise(prix: float, pourcentage: float) -> float:
    if not 0 <= pourcentage <= 100:
        raise ValueError("Pourcentage invalide")
    return round(prix * (1 - pourcentage / 100), 2)
tests/test_prix.py
import pytest
from boutique.prix import appliquer_remise
 
def test_remise_simple():
    assert appliquer_remise(100, 20) == 80
 
def test_remise_invalide():
    with pytest.raises(ValueError, match="invalide"):
        appliquer_remise(100, 150)
 
@pytest.mark.parametrize("prix, pct, attendu", [
    (100, 0, 100),
    (100, 100, 0),
    (19.99, 10, 17.99),
])
def test_remise_cas(prix, pct, attendu):
    assert appliquer_remise(prix, pct) == attendu
uv run pytest -v
uv run pytest --cov=boutique --cov-report=term-missing

pytest réécrit les assert pour afficher les valeurs comparées en cas d'échec. Pas besoin de assertEqual.

Fixtures

Une fixture prépare un contexte réutilisable. pytest l'injecte par nom de paramètre.

tests/conftest.py
import sqlite3
import pytest
 
@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")
    yield conn
    conn.close()
 
def test_insertion(db):
    db.execute("INSERT INTO users (email) VALUES ('a@b.c')")
    assert db.execute("SELECT COUNT(*) FROM users").fetchone()[0] == 1

Fixtures intégrées très utiles : tmp_path (dossier temporaire), monkeypatch (remplacer variables d'environnement ou attributs), capsys (capturer stdout).

def test_config(monkeypatch, tmp_path):
    monkeypatch.setenv("APP_ENV", "test")
    (tmp_path / "config.toml").write_text('debug = true')
    ...

Mocker les dépendances externes

from unittest.mock import patch
 
def test_meteo_api_down():
    with patch("app.meteo.httpx.get", side_effect=TimeoutError):
        assert recuperer_meteo("Paris") is None

Règle : on mocke là où le nom est utilisé (app.meteo.httpx), pas là où il est défini.

Tests basés sur les propriétés

Hypothesis génère des centaines d'entrées aléatoires et réduit automatiquement le cas d'échec minimal :

from hypothesis import given, strategies as st
 
@given(st.floats(min_value=0, max_value=1e6), st.floats(min_value=0, max_value=100))
def test_remise_jamais_negative(prix, pct):
    assert appliquer_remise(prix, pct) >= 0

Hypothesis m'a déjà trouvé des bugs d'arrondi que je n'aurais jamais pensé à tester à la main.

Tout automatiser

pyproject.toml
[tool.ruff]
line-length = 100
 
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
 
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"

Ces trois commandes tournent dans la CI à chaque push :

uv run ruff check .
uv run mypy src
uv run pytest

La mise en place d'un pipeline qui les exécute est détaillée dans le guide GitHub Actions pour la CI/CD.


Types pour attraper les erreurs avant l'exécution, Pydantic pour les données extérieures, pytest pour le comportement. Ce trio donne à Python la robustesse qu'on lui reproche souvent de ne pas avoir.

Précédent
Itérateurs, générateurs, décorateurs et context managers
Suivant
Concurrence : threads, processus et asyncio

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main