L'injection de dépendances : une méthode Python
Modèles de conception DI Python pour un code propre et testable
L’injection de dépendances (DI) est un motif de conception fondamental qui favorise un code propre, testable et maintenable dans les applications Python.
Que vous construisiez des API REST avec FastAPI, que vous implémentiez des tests unitaires, ou que vous travailliez avec des fonctions AWS Lambda, comprendre l’injection de dépendances améliorera significativement la qualité de votre code.

Qu’est-ce que l’injection de dépendances ?
L’injection de dépendances est un motif de conception où les composants reçoivent leurs dépendances de sources externes plutôt que de les créer en interne. Cette approche découple les composants, rendant votre code plus modulaire, testable et maintenable.
En Python, l’injection de dépendances est particulièrement puissante grâce à la nature dynamique du langage et son support des protocoles, des classes de base abstraites et du typage canard. La flexibilité de Python signifie que vous pouvez implémenter des motifs DI sans cadres lourds, bien que des cadres soient disponibles lorsque nécessaire.
Pourquoi utiliser l’injection de dépendances en Python ?
Amélioration de la testabilité : En injectant des dépendances, vous pouvez facilement remplacer les implémentations réelles par des mocks ou des doubles de test. Cela vous permet d’écrire des tests unitaires qui sont rapides, isolés et n’ont pas besoin de services externes comme des bases de données ou des API. Lors de la rédaction de tests unitaires complets, l’injection de dépendances rend trivial le remplacement des dépendances réelles par des doubles de test.
Meilleure maintenabilité : Les dépendances deviennent explicites dans votre code. Lorsque vous regardez un constructeur, vous voyez immédiatement ce dont un composant a besoin. Cela rend la base de code plus facile à comprendre et à modifier.
Couplage faible : Les composants dépendent d’abstractions (protocoles ou ABC) plutôt que d’implémentations concrètes. Cela signifie que vous pouvez changer les implémentations sans affecter le code dépendant.
Flexibilité : Vous pouvez configurer différentes implémentations pour différents environnements (développement, test, production) sans modifier votre logique métier. Cela est particulièrement utile lors du déploiement d’applications Python sur différentes plateformes, qu’il s’agisse de AWS Lambda ou de serveurs traditionnels.
Injection de constructeur : la manière Python
La manière la plus courante et idiomatique de mettre en œuvre l’injection de dépendances en Python est par l’injection de constructeur - accepter les dépendances comme paramètres dans la méthode __init__.
Exemple de base
Voici un exemple simple démontrant l’injection de constructeur :
from typing import Protocol
from abc import ABC, abstractmethod
# Définir un protocole pour le dépôt
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Le service dépend du protocole du dépôt
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, user_id: int) -> 'User | None':
return self.repo.find_by_id(user_id)
Ce motif rend clair que UserService nécessite un UserRepository. Vous ne pouvez pas créer un UserService sans fournir un dépôt, ce qui empêche les erreurs d’exécution dues à des dépendances manquantes.
Multiples dépendances
Lorsque qu’un composant a plusieurs dépendances, ajoutez-les simplement comme paramètres de constructeur :
class EmailService(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
class Logger(Protocol):
def info(self, msg: str) -> None:
...
def error(self, msg: str, err: Exception) -> None:
...
class OrderService:
def __init__(
self,
repo: OrderRepository,
email_svc: EmailService,
logger: Logger,
payment_svc: PaymentService,
):
self.repo = repo
self.email_svc = email_svc
self.logger = logger
self.payment_svc = payment_svc
Utilisation des protocoles et des classes de base abstraites
L’un des principes clés lors de la mise en œuvre de l’injection de dépendances est le principe d’inversion des dépendances (DIP) : les modules de haut niveau ne doivent pas dépendre des modules de bas niveau ; les deux doivent dépendre des abstractions.
En Python, vous pouvez définir des abstractions en utilisant soit les Protocoles (typage structurel) soit les Classes de Base Abstraites (ABC) (typage nominal).
Protocoles (Python 3.8+)
Les protocoles utilisent le typage structurel - si un objet a les méthodes requises, il satisfait le protocole :
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Toute classe avec la méthode process_payment satisfait ce protocole
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Logique de carte de crédit
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Logique PayPal
return True
# Le service accepte tout PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Classes de base abstraites
Les ABC utilisent le typage nominal - les classes doivent explicitement hériter de l’ABC :
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
return True
Quand utiliser les Protocoles vs les ABC : Utilisez les Protocoles lorsque vous voulez un typage structurel et une flexibilité. Utilisez les ABC lorsque vous avez besoin d’imposer des hiérarchies d’héritage ou de fournir des implémentations par défaut.
Exemple du monde réel : Abstraction de base de données
Lors du travail avec des bases de données dans les applications Python, vous aurez souvent besoin d’abstraire les opérations de base de données. Voici comment l’injection de dépendances aide :
from typing import Protocol, Optional
from contextlib import contextmanager
class Database(Protocol):
@contextmanager
def transaction(self):
...
def execute(self, query: str, params: dict) -> None:
...
def fetch_one(self, query: str, params: dict) -> Optional[dict]:
...
# Le dépôt dépend de l'abstraction
class UserRepository:
def __init__(self, db: Database):
self.db = db
def find_by_id(self, user_id: int) -> Optional['User']:
result = self.db.fetch_one(
"SELECT * FROM users WHERE id = :id",
{"id": user_id}
)
if result:
return User(**result)
return None
Ce motif vous permet de changer les implémentations de base de données (PostgreSQL, SQLite, MongoDB) sans modifier votre code de dépôt.
Le motif de la racine de composition
La racine de composition est l’endroit où vous assemblez toutes vos dépendances au point d’entrée de l’application (généralement main.py ou votre usine d’application). Cela centralise la configuration des dépendances et rend le graphe de dépendances explicite.
def create_app() -> FastAPI:
app = FastAPI()
# Initialiser les dépendances d'infrastructure
db = init_database()
logger = init_logger()
# Initialiser les dépôts
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Initialiser les services avec les dépendances
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Initialiser les gestionnaires HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Connecter les routes
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Cette approche rend clair comment votre application est structurée et d’où viennent les dépendances. Elle est particulièrement précieuse lorsque vous construisez des applications suivant les principes de l’architecture propre, où vous devez coordonner plusieurs couches de dépendances.
Cadres d’injection de dépendances
Pour les applications plus grandes avec des graphes de dépendances complexes, la gestion manuelle des dépendances peut devenir fastidieuse. Python dispose de plusieurs cadres DI qui peuvent aider :
Dependency Injector
Dependency Injector est un cadre populaire qui fournit une approche basée sur un conteneur pour l’injection de dépendances.
Installation :
pip install dependency-injector
Exemple :
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Configuration
config = providers.Configuration()
# Base de données
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Dépôts
user_repository = providers.Factory(
UserRepository,
db=db
)
# Services
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Utilisation
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector est une bibliothèque légère inspirée de Google’s Guice, axée sur la simplicité.
Installation :
pip install injector
Exemple :
from injector import Injector, inject, Module, provider
class DatabaseModule(Module):
@provider
def provide_db(self) -> Database:
return Database(connection_string="...")
class UserModule(Module):
@inject
def __init__(self, repo: UserRepository):
self.repo = repo
injector = Injector([DatabaseModule()])
user_service = injector.get(UserService)
Quand utiliser les cadres
Utilisez un cadre lorsque :
- Votre graphe de dépendances est complexe avec de nombreux composants interdépendants
- Vous avez plusieurs implémentations de la même interface qui doivent être sélectionnées en fonction de la configuration
- Vous voulez une résolution automatique des dépendances
- Vous construisez une grande application où le câblage manuel devient source d’erreurs
Restez avec l’injection manuelle lorsque :
- Votre application est de petite à moyenne taille
- Le graphe de dépendances est simple et facile à suivre
- Vous voulez garder les dépendances minimales et explicites
- Vous préférez le code explicite à la magie du cadre
Test avec l’injection de dépendances
L’un des principaux avantages de l’injection de dépendances est l’amélioration de la testabilité. Voici comment l’ID facilite les tests :
Exemple de test unitaire
from unittest.mock import Mock
import pytest
# Implémentation mock pour les tests
class MockUserRepository:
def __init__(self):
self.users = {}
self.error = None
def find_by_id(self, user_id: int) -> Optional['User']:
if self.error:
raise self.error
return self.users.get(user_id)
def save(self, user: 'User') -> 'User':
if self.error:
raise self.error
self.users[user.id] = user
return user
# Test utilisant le mock
def test_user_service_get_user():
mock_repo = MockUserRepository()
mock_repo.users[1] = User(id=1, name="John", email="john@example.com")
service = UserService(mock_repo)
user = service.get_user(1)
assert user is not None
assert user.name == "John"
Ce test s’exécute rapidement, ne nécessite pas de base de données et teste votre logique métier en isolation. Lors du travail avec les tests unitaires en Python, l’injection de dépendances facilite la création de doubles de test et la vérification des interactions.
Utilisation des fixtures pytest
Les fixtures pytest fonctionnent parfaitement avec l’injection de dépendances :
@pytest.fixture
def mock_user_repository():
return MockUserRepository()
@pytest.fixture
def user_service(mock_user_repository):
return UserService(mock_user_repository)
def test_user_service_get_user(user_service, mock_user_repository):
user = User(id=1, name="John", email="john@example.com")
mock_user_repository.users[1] = user
result = user_service.get_user(1)
assert result.name == "John"
Modèles courants et bonnes pratiques
1. Utiliser la ségrégation d’interface
Gardez les protocoles et interfaces petits et concentrés sur ce dont le client a réellement besoin :
# Bon : Le client n'a besoin que de lire les utilisateurs
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Interface séparée pour l'écriture
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Valider les dépendances dans les constructeurs
Les constructeurs doivent valider les dépendances et lever des erreurs claires si l’initialisation échoue :
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("le dépôt utilisateur ne peut pas être None")
self.repo = repo
3. Utiliser les indications de type
Les indications de type rendent les dépendances explicites et aident avec le support de l’IDE et la vérification de type statique :
from typing import Protocol, Optional
class UserService:
def __init__(
self,
repo: UserRepository,
logger: Logger,
email_service: EmailService,
) -> None:
self.repo = repo
self.logger = logger
self.email_service = email_service
4. Éviter la sur-injection
N’injectez pas les dépendances qui sont de véritables détails d’implémentation interne. Si un composant crée et gère ses propres objets auxiliaires, c’est bien :
# Bon : L'auxiliaire interne n'a pas besoin d'injection
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Cache interne - n'a pas besoin d'injection
self._cache: dict[int, User] = {}
5. Documenter les dépendances
Utilisez les docstrings pour documenter pourquoi les dépendances sont nécessaires et toute contrainte :
class UserService:
"""UserService gère la logique métier liée aux utilisateurs.
Args:
repo: UserRepository pour l'accès aux données. Doit être thread-safe
s'il est utilisé dans des contextes concurrents.
logger: Logger pour le suivi des erreurs et le débogage.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Injection de dépendances avec FastAPI
FastAPI a un support intégré pour l’injection de dépendances via son mécanisme Depends :
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
def get_user_repository() -> UserRepository:
db = get_database()
return UserRepository(db)
def get_user_service(
repo: Annotated[UserRepository, Depends(get_user_repository)]
) -> UserService:
return UserService(repo)
@app.get("/users/{user_id}")
def get_user(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)]
):
user = service.get_user(user_id)
if not user:
raise HTTPException(status_code=404)
return user
Le système d’injection de dépendances de FastAPI gère automatiquement le graphe de dépendances, facilitant la création d’API propres et maintenables.
Quand NE PAS utiliser l’injection de dépendances
L’injection de dépendances est un outil puissant, mais elle n’est pas toujours nécessaire :
Sauter l’ID pour :
- Les objets de valeur simples ou les classes de données
- Les fonctions auxiliaires internes ou les utilitaires
- Les scripts ponctuels ou les petits utilitaires
- Lorsque l’instanciation directe est plus claire et plus simple
Exemple de cas où NE PAS utiliser l’ID :
# Classe de données simple - pas besoin d'ID
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Utilitaire simple - pas besoin d'ID
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Intégration avec l’écosystème Python
L’injection de dépendances fonctionne de manière transparente avec d’autres modèles et outils Python. Lors de la construction d’applications utilisant les packages Python ou les frameworks de tests unitaires, vous pouvez injecter ces services dans votre logique métier :
class ReportService:
def __init__(
self,
pdf_generator: PDFGenerator,
repo: ReportRepository,
logger: Logger,
):
self.pdf_generator = pdf_generator
self.repo = repo
self.logger = logger
def generate_report(self, report_id: int) -> bytes:
report_data = self.repo.get_by_id(report_id)
pdf = self.pdf_generator.generate(report_data)
self.logger.info(f"Generated report {report_id}")
return pdf
Cela permet de remplacer les implémentations ou d’utiliser des mocks lors des tests.
Injection de dépendances asynchrone
La syntaxe async/await de Python fonctionne bien avec l’injection de dépendances :
from typing import Protocol
import asyncio
class AsyncUserRepository(Protocol):
async def find_by_id(self, user_id: int) -> Optional['User']:
...
async def save(self, user: 'User') -> 'User':
...
class AsyncUserService:
def __init__(self, repo: AsyncUserRepository):
self.repo = repo
async def get_user(self, user_id: int) -> Optional['User']:
return await self.repo.find_by_id(user_id)
async def get_users_batch(self, user_ids: list[int]) -> list['User']:
tasks = [self.repo.find_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
Conclusion
L’injection de dépendances est un pilier pour écrire du code Python maintenable et testable. En suivant les modèles décrits dans cet article - injection de constructeur, conception basée sur les protocoles et le modèle de racine de composition - vous créerez des applications plus faciles à comprendre, tester et modifier.
Commencez par l’injection de constructeur manuelle pour les petites à moyennes applications, et envisagez des frameworks comme dependency-injector ou injector lorsque votre graphe de dépendances grandit. Rappelez-vous que l’objectif est la clarté et la testabilité, pas la complexité pour elle-même.
Pour plus de ressources de développement Python, consultez notre Python Cheatsheet pour une référence rapide sur la syntaxe Python et les modèles courants.
Liens utiles
- Python Cheatsheet
- Tests unitaires en Python
- Modèles de conception Python pour une architecture propre
- Sortie structurée - LLMs sur Ollama avec Qwen3 - Python et Go
- Comparaison de la sortie structurée parmi les principaux fournisseurs de LLM - OpenAI, Gemini, Anthropic, Mistral et AWS Bedrock
- Construction d’une AWS Lambda en mode double avec Python et Terraform
Ressources externes
- Injection de dépendances en Python - Real Python
- Comment l’injection de dépendances en Python améliore la structure du code - Volito Digital
- Tutoriel d’injection de dépendances en Python - DataCamp
- Documentation officielle de Dependency Injector
- Injector - Framework DI léger
- Dépendances FastAPI - Documentation officielle
- Principes SOLID en Python - Lexique des modèles logiciels
- Protocoles Python et sous-typage structurel - PEP 544