Chapitre 17
Chapitre 17 : Exercices Pratiques
> **40 exercices couvrant l'ensemble du programme** — de la syntaxe Python à l'architecture en production.
Chapitre 17 — Exercices Pratiques
Version : 1.0.0
Auteur : Modern Python Engineering Team
Prérequis : Python ≥ 3.11, notions des chapitres 1-16
Table des matières
- Introduction
- Module 1 : Python Basics (Exercices 01–08)
- Module 2 : Async (Exercices 09–14)
- Module 3 : pytest (Exercices 15–19)
- Module 4 : FastAPI (Exercices 20–24)
- Module 5 : SQLAlchemy (Exercices 25–28)
- Module 6 : pandas (Exercices 29–32)
- Module 7 : scikit-learn (Exercices 33–35)
- Module 8 : Architecture & CLI (Exercices 36–38)
- Module 9 : Docker (Exercices 39–40)
- Ressources complémentaires
Introduction
Ce chapitre contient 40 exercices progressifs qui couvrent l'intégralité du programme Modern Python Engineering. Chaque exercice est accompagné :
- D'un énoncé détaillé avec contexte et attentes.
- D'un jeu de tests pytest pour valider votre solution.
- D'une correction commentée dans le chapitre 19.
- D'une grille d'auto-évaluation.
Notation et conventions
# Les fonctions à implémenter sont annotées avec des types
def solve(data: list[int]) -> dict[str, float]: ...
# Les tests sont fournis dans tests/test_exNN.py
# Lancez-les avec :
# pytest tests/test_ex01.py -v
Structure d'un exercice
Chaque exercice se décompose en :
- Contexte : mise en situation réaliste (ex. "Vous devez parser un fichier CSV")
- Spécification : ce que doit faire la fonction/classe
- Contraintes : règles à respecter (pas de librairie externe, complexité O(n), etc.)
- Validation : commande pytest à exécuter
- Pour aller plus loin : variantes suggérées
Module 1 : Python Basics (Exercices 01–08)
Exercice 01 : Typage statique et validation
Contexte : Vous développez une librairie de validation de formulaires.
Spécification : Implémentez une fonction validate_email(email: str) -> bool
qui vérifie qu'une adresse email respecte le format RFC 5322 simplifié.
def validate_email(email: str) -> bool:
"""Validate email format using RFC 5322 simplified rules.
Args:
email: The email string to validate.
Returns:
True if valid, False otherwise.
"""
# À compléter
Contraintes :
- N'utilisez pas
re.match— implémentez l'algorithme manuellement. - Gérez les cas limites :
"","@","a@b"," user@domain ".
Validation :
pytest tests/test_ex01.py -v
Pour aller plus loin : Ajoutez la validation des domaines avec vérification DNS.
Exercice 02 : Structures de données avancées
Contexte : Vous devez implémenter un cache LRU (Least Recently Used) sans
utiliser functools.lru_cache.
Spécification :
class LRUCache[T: Hashable, U]:
"""LRU cache with O(1) get/put operations.
Args:
capacity: Maximum number of items.
"""
def __init__(self, capacity: int) -> None: ...
def get(self, key: T) -> U | None: ...
def put(self, key: T, value: U) -> None: ...
Contraintes :
- Complexité O(1) pour
getetput. - Utilisez
OrderedDictou unedictdoublée d'unelistchaînée. - Le cache doit être thread-safe (utilisez
threading.Lock).
Tests clés :
def test_lru_eviction():
cache = LRUCache[str, int](2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a") # "a" devient récent
cache.put("c", 3) # "b" est évincé
assert cache.get("b") is None
assert cache.get("c") == 3
Exercice 03 : Itérateurs personnalisés
Contexte : Créez un itérateur qui génère la séquence de Fibonacci de manière paresseuse et potentiellement infinie.
class Fibonacci:
"""Iterator that yields Fibonacci numbers up to max_value."""
def __init__(self, max_value: int | None = None) -> None: ...
def __iter__(self) -> Self: ...
def __next__(self) -> int: ...
Pour aller plus loin : Implémentez __getitem__ pour permettre l'indexation
fib[10] et le slicing fib[5:15].
Exercice 04 : Générateurs et expressions génératrices
Contexte : Traitez un flux de logs (fichier de plusieurs Go) sans le charger en mémoire.
def parse_log_stream(
filepath: Path,
level: str = "ERROR",
) -> Generator[dict[str, Any], None, None]:
"""Yield parsed log entries of the specified level.
Format de log attendu :
[TIMESTAMP] LEVEL module: message
"""
Contraintes :
- Utilisez
yieldpour produire les entrées une par une. - Ne chargez jamais plus d'une ligne en mémoire.
Exercice 05 : Décorateurs paramétrés
Contexte : Implémentez un décorateur de retry avec backoff exponentiel.
def retry(
max_attempts: int = 3,
delay: float = 1.0,
backoff: float = 2.0,
exceptions: tuple[type[Exception], ...] = (Exception,),
) -> Callable[..., Callable[..., Any]]:
"""Decorator that retries a function on failure.
Args:
max_attempts: Maximum number of attempts.
delay: Initial delay in seconds.
backoff: Multiplier applied to delay after each attempt.
exceptions: Tuple of exception types to catch.
"""
Exemple d'utilisation :
@retry(max_attempts=5, delay=0.1, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> bytes: ...
Exercice 06 : Context managers
Contexte : Implémentez un context manager qui mesure le temps d'exécution d'un bloc de code, avec un rapport optionnel.
@contextmanager
def timed(label: str = "", report: bool = True) -> Generator[float, None, None]:
"""Context manager that times code execution.
Yields:
The elapsed time in seconds when exiting the context.
"""
Exercice 07 : Dataclasses et validation
Contexte : Modélisez un système de réservation de vols avec validation.
@dataclass(frozen=True, slots=True)
class Booking:
passenger_name: str
flight_number: str
seat: str | None = None
extra_baggage: bool = False
Validation :
passenger_name: 2-100 caractères, lettres uniquementflight_number: patternXX1234(2 lettres + 4 chiffres)seat: optionnel, pattern12A(nombre + lettre)
Utilisez __post_init__ pour les validations.
Exercice 08 : Programmation fonctionnelle
Contexte : Traitez une liste de transactions bancaires avec map, filter,
reduce et itertools.
Transaction = namedtuple("Transaction", ["date", "amount", "category"])
def summarize_by_category(
transactions: list[Transaction],
) -> dict[str, float]:
"""Summarize transactions by category using functional programming."""
Contraintes : Pas de boucles for ou while. Utilisez exclusivement
map, filter, functools.reduce et itertools.groupby.
Module 2 : Async (Exercices 09–14)
Exercice 09 : Coroutines de base
Contexte : Simulez des appels API asynchrones avec des délais variables.
async def fetch_url(url: str, delay: float = 1.0) -> dict[str, Any]:
"""Simulate fetching a URL with a given delay."""
await asyncio.sleep(delay)
return {"url": url, "status": 200, "data": f"Content of {url}"}
async def fetch_all(urls: list[str]) -> list[dict[str, Any]]:
"""Fetch all URLs concurrently using asyncio.gather."""
Exercice 10 : asyncio.gather et gestion d'erreurs
Contexte : Implémentez un gather avec gestion fine des erreurs : au lieu
d'annuler toutes les tâches à la première erreur, collectez les résultats
partiels.
async def gather_with_errors(
*coros: Awaitable[Any],
return_exceptions: bool = True,
) -> list[Any]:
"""Gather results, collecting exceptions without cancelling others."""
Exercice 11 : Async Queue et producteur-consommateur
Contexte : Implémentez un pipeline de traitement d'images avec une file d'attente asynchrone.
async def producer(queue: asyncio.Queue[ImageTask], images: list[Path]) -> None: ...
async def worker(
queue: asyncio.Queue[ImageTask],
result_queue: asyncio.Queue[Result],
worker_id: int,
) -> None: ...
async def consumer(result_queue: asyncio.Queue[Result]) -> None: ...
Exercice 12 : Timeout et cancellation
Contexte : Implémentez une fonction avec timeout et fallback.
async def fetch_with_timeout(
url: str,
timeout: float = 5.0,
fallback: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Fetch URL with timeout. Return fallback if timeout occurs."""
Utilisez asyncio.timeout (Python 3.12+) ou asyncio.wait_for.
Exercice 13 : aiofiles et I/O asynchrone
Contexte : Traitez un répertoire de fichiers CSV asynchrone.
async def process_csv_files(
directory: Path,
chunk_size: int = 8192,
) -> AsyncIterator[dict[str, str]]:
"""Read and parse CSV files asynchronously using aiofiles."""
Exercice 14 : AnyIO — library-agnostic async
Contexte : Réécrivez les exercices 09-13 en utilisant AnyIO pour être
compatible avec asyncio et trio.
import anyio
async def run_pipeline() -> None:
async with anyio.create_task_group() as tg:
tg.start_soon(fetch_url, "https://example.com")
Module 3 : pytest (Exercices 15–19)
Exercice 15 : Fixtures et scopes
Contexte : Créez une fixture pour une base de données SQLite en mémoire.
@pytest.fixture
def db_connection():
"""Provide a fresh SQLite in-memory database for each test."""
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
yield conn
conn.close()
Niveaux :
@pytest.fixturesimplescope="module"scope="session"autouse=True
Exercice 16 : Mocking avec unittest.mock
Contexte : Testez une fonction qui appelle une API externe.
def get_weather(city: str) -> dict[str, Any]:
response = requests.get(f"https://api.weather.com/v1/{city}")
response.raise_for_status()
return response.json()
# À tester sans appeler l'API réelle
def test_get_weather(httpx_mock: HTTPXMock) -> None: ...
Exercice 17 : Paramétrage avancé
Contexte : Testez une fonction avec 20 combinaisons d'entrées.
@pytest.mark.parametrize(
"input_data,expected",
[
([1, 2, 3], 6),
([], 0),
([-1, 0, 1], 0),
pytest.param([1], 1, id="single_element"),
pytest.param(
[10**6] * 1000, 10**9,
marks=pytest.mark.slow,
id="large_input",
),
],
)
def test_sum_list(input_data: list[int], expected: int) -> None:
assert sum_list(input_data) == expected
Exercice 18 : Tests de propriétés avec Hypothesis
Contexte : Vérifiez que votre implémentation de LRU cache est correcte via des tests basés sur les propriétés.
from hypothesis import given, strategies as st
@given(st.lists(st.integers()), st.integers(min_value=1, max_value=100))
def test_lru_cache_properties(items: list[int], capacity: int) -> None:
cache = LRUCache[int, int](capacity)
# Vérifier que get(put(x)) == x
for x in items:
cache.put(x, x)
assert cache.get(x) == x
Exercice 19 : Tests de non-régression et coverage
Contexte : Établissez une suite de tests de non-régression pour un module de calcul financier avec un coverage ≥ 95 %.
def test_coverage_threshold() -> None:
"""Vérifier que le coverage est ≥ 95 %."""
# Utiliser pytest-cov --fail-under=95
pass
Module 4 : FastAPI (Exercices 20–24)
Exercice 20 : CRUD REST de base
Contexte : Implémentez une API de gestion de tâches (Todo list).
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class TodoCreate(BaseModel):
title: str
description: str = ""
class Todo(TodoCreate):
id: int
completed: bool = False
@app.post("/todos", response_model=Todo, status_code=201)
async def create_todo(todo: TodoCreate) -> Todo: ...
@app.get("/todos")
async def list_todos() -> list[Todo]: ...
@app.get("/todos/{todo_id}")
async def get_todo(todo_id: int) -> Todo: ...
@app.put("/todos/{todo_id}")
async def update_todo(todo_id: int, todo: TodoCreate) -> Todo: ...
@app.delete("/todos/{todo_id}", status_code=204)
async def delete_todo(todo_id: int) -> None: ...
Exercice 21 : Validation avancée et dépendances
Contexte : Ajoutez validation, pagination, tri et dépendances.
from fastapi import Depends, Query
async def pagination_params(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
) -> tuple[int, int]: ...
@app.get("/todos")
async def list_todos(
pagination: tuple[int, int] = Depends(pagination_params),
completed: bool | None = None,
) -> list[Todo]: ...
Exercice 22 : Authentification et sécurité
Contexte : Implémentez JWT authentication avec OAuth2 Password Flow.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
payload = decode_jwt(token)
return await get_user(payload["sub"])
Exercice 23 : WebSocket temps réel
Contexte : Implémentez un chat temps réel avec WebSocket.
@app.websocket("/ws/chat/{room_id}")
async def chat_websocket(
websocket: WebSocket,
room_id: str,
) -> None:
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo: {data}")
Exercice 24 : Tests d'API avec TestClient
Contexte : Écrivez des tests complets pour l'API Todo.
from fastapi.testclient import TestClient
client = TestClient(app)
def test_create_todo():
response = client.post("/todos", json={"title": "Learn FastAPI"})
assert response.status_code == 201
data = response.json()
assert data["title"] == "Learn FastAPI"
assert "id" in data
Module 5 : SQLAlchemy (Exercices 25–28)
Exercice 25 : ORM — Modèles et relations
Contexte : Modélisez un blog avec utilisateurs, articles et commentaires.
from sqlalchemy.orm import Mapped, mapped_column, relationship
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
posts: Mapped[list["Post"]] = relationship(back_populates="author")
Exercice 26 : Core — Requêtes SQL sans ORM
Contexte : Exécutez des requêtes avancées avec SQLAlchemy Core.
from sqlalchemy import text, select, func
async def get_top_authors(session: AsyncSession, limit: int = 10) -> list[dict]:
stmt = (
select(User.name, func.count(Post.id).label("post_count"))
.join(Post, User.id == Post.author_id)
.group_by(User.id)
.order_by(func.count(Post.id).desc())
.limit(limit)
)
result = await session.execute(stmt)
return result.mappings().all()
Exercice 27 : Alembic — Migrations
Contexte : Créez et gérez des migrations de schéma.
alembic init alembic
alembic revision --autogenerate -m "add users table"
alembic upgrade head
Exercice 28 : Async Session et unit of work
Contexte : Implémentez un pattern Unit of Work avec session async.
class UnitOfWork:
def __init__(self, session_factory: AsyncSessionFactory) -> None:
self.session_factory = session_factory
async def __aenter__(self) -> Self:
self.session: AsyncSession = self.session_factory()
return self
async def __aexit__(self, *args: Any) -> None:
await self.session.close()
Module 6 : pandas (Exercices 29–32)
Exercice 29 : DataFrames et nettoyage
Contexte : Nettoyez un dataset de ventes avec valeurs manquantes et outliers.
def clean_sales_data(df: pd.DataFrame) -> pd.DataFrame:
"""Clean sales DataFrame:
- Remove rows with missing critical fields
- Cap outliers at 99th percentile
- Standardize date formats
- Remove duplicates
"""
Exercice 30 : GroupBy et agrégations
Contexte : Analysez les performances commerciales par région et catégorie.
def monthly_summary(df: pd.DataFrame) -> pd.DataFrame:
return (
df.groupby([pd.Grouper(key="date", freq="ME"), "region", "category"])
.agg(total_sales=("amount", "sum"), count=("id", "nunique"))
.reset_index()
)
Exercice 31 : Merge et Jointures
Contexte : Combinez 3 DataFrames (commandes, clients, produits) en une vue unique.
def build_customer_view(
orders: pd.DataFrame,
customers: pd.DataFrame,
products: pd.DataFrame,
) -> pd.DataFrame:
return (
orders
.merge(customers, on="customer_id", how="left")
.merge(products, on="product_id", how="left")
)
Exercice 32 : Time Series
Contexte : Analysez une série temporelle de prix d'actions avec rééchantillonnage et fenêtres glissantes.
def resample_ohlc(df: pd.DataFrame, freq: str = "1D") -> pd.DataFrame:
return df.resample(freq).agg({
"price": ["first", "max", "min", "last"],
"volume": "sum",
})
Module 7 : scikit-learn (Exercices 33–35)
Exercice 33 : Pipeline de classification
Contexte : Construisez un pipeline complet de classification pour le dataset Iris.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
pipeline = Pipeline([
("scaler", StandardScaler()),
("classifier", RandomForestClassifier(
n_estimators=100, random_state=42
)),
])
Exercice 34 : Grid Search et validation croisée
Contexte : Optimisez les hyperparamètres d'un modèle XGBoost.
from sklearn.model_selection import GridSearchCV
param_grid = {
"classifier__n_estimators": [100, 200],
"classifier__max_depth": [3, 5, 7],
"classifier__learning_rate": [0.01, 0.1],
}
grid = GridSearchCV(pipeline, param_grid, cv=5, scoring="f1")
grid.fit(X_train, y_train)
Exercice 35 : Feature engineering et sélection
Contexte : Créez un pipeline de feature engineering pour données tabulaires.
class FeatureEngineer(TransformerMixin, BaseEstimator):
def __init__(self, add_polynomials: bool = True) -> None:
self.add_polynomials = add_polynomials
def fit(self, X: np.ndarray, y: np.ndarray | None = None) -> Self: ...
def transform(self, X: np.ndarray) -> np.ndarray: ...
Module 8 : Architecture & CLI (Exercices 36–38)
Exercice 36 : Clean Architecture
Contexte : Organisez un projet selon les principes de clean architecture.
src/
├── domain/ # Entities, value objects
│ ├── entities.py
│ └── value_objects.py
├── application/ # Use cases, ports
│ ├── use_cases.py
│ └── ports.py
├── infrastructure/ # Adapters (DB, API, etc.)
│ ├── database.py
│ └── repositories.py
└── interfaces/ # Entry points (API, CLI)
├── api/
└── cli/
Exercice 37 : CLI avec Typer
Contexte : Créez un outil CLI pour analyser des fichiers de log.
import typer
from rich.console import Console
from rich.table import Table
app = typer.Typer()
@app.command()
def analyze(
file: Path = typer.Argument(..., help="Path to log file"),
level: str = typer.Option("ERROR", "--level", "-l"),
output: Path | None = typer.Option(None, "--output", "-o"),
) -> None: ...
Exercice 38 : Injection de dépendances
Contexte : Implémentez un conteneur DI et l'appliquez à l'architecture clean.
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
database = providers.Singleton(Database, url=config.db.url)
repository = providers.Factory(UserRepository, session=database.provided.session)
use_case = providers.Factory(CreateUserUseCase, repo=repository)
Module 9 : Docker (Exercices 39–40)
Exercice 39 : Dockerfile multi-stage
Contexte : Créez un Dockerfile multi-stage pour l'API FastAPI du module 4.
FROM python:3.13-slim AS builder
# Build dependencies
FROM python:3.13-slim AS production
# Runtime only
FROM production AS development
# Dev tooling + hot reload
Exercice 40 : docker-compose avec dépendances
Contexte : Créez un docker-compose avec FastAPI, PostgreSQL, Redis, et Celery worker.
version: "3.9"
services:
api:
build:
target: development
ports: ["8000:8000"]
depends_on: [db, redis]
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
redis:
image: redis:7-alpine
worker:
build:
target: development
command: celery -A src.worker worker -l info
depends_on: [redis]
Ressources complémentaires
- Python Typing Cheat Sheet
- FastAPI Best Practices
- SQLAlchemy 2.0 Documentation
- pytest Documentation
- scikit-learn User Guide
- Docker Best Practices
Prochain chapitre : 18-QCM →
Évaluez vos connaissances avec 100 questions à choix multiples couvrant l'ensemble du programme.