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.
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é.
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.
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: boolAcceptez 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.
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 mypyEt les alias :
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | Noneuv add --dev mypy
uv run mypy src --strict[tool.mypy]
python_version = "3.13"
strict = true
warn_unreachable = trueSur 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.
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 champsFastAPI repose entièrement sur ce mécanisme.
uv add --dev pytest pytest-covUn test est une fonction préfixée test_ qui utilise assert :
def appliquer_remise(prix: float, pourcentage: float) -> float:
if not 0 <= pourcentage <= 100:
raise ValueError("Pourcentage invalide")
return round(prix * (1 - pourcentage / 100), 2)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) == attenduuv run pytest -v
uv run pytest --cov=boutique --cov-report=term-missingpytest réécrit les assert pour afficher les valeurs comparées en cas d'échec. Pas besoin de assertEqual.
Une fixture prépare un contexte réutilisable. pytest l'injecte par nom de paramètre.
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] == 1Fixtures 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')
...from unittest.mock import patch
def test_meteo_api_down():
with patch("app.meteo.httpx.get", side_effect=TimeoutError):
assert recuperer_meteo("Paris") is NoneRègle : on mocke là où le nom est utilisé (app.meteo.httpx), pas là où il est défini.
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) >= 0Hypothesis m'a déjà trouvé des bugs d'arrondi que je n'aurais jamais pensé à tester à la main.
[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 pytestLa 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.