> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# FastAPI + Injector : une approche simplifiée de l’injection de dépendances en Python
- URL: https://www.sfeir.dev/back/di-en-python/
- Published: 2025-08-12T09:52:11.000Z
- Updated: 2025-08-12T09:52:11.000Z
- Description: L'injection de dépendances ne devrait pas être une souffrance ! Je vous présente une alternative efficace pour réussir l'inversion de contrôle dans FastAPI, en utilisant la bibliothèque injector.
- Author: Ulises Hernández Calzadillas
- Tags: Python, Back, SOLID, Clean-code, clean architecture, Uncle Bob

Python est un langage **dynamiquement** typé, ce qui signifie que la vérification des types se fait à l'exécution—contrairement aux langages statiquement typés comme C# ou Java. Cette flexibilité permet des fonctionnalités puissantes telles que le *monkey patching*, le *hot swapping* et la création dynamique de classes. Pour cette raison, l’**injection de dépendances** (*dependency injection* ou **DI** en anglais) n’est pas strictement nécessaire dans les applications Python.

Cependant, la **DI** apporte une structure formelle à votre base de code, en particulier lorsque vous utilisez des modèles architecturaux comme le **Domain-Driven Design** (**DDD**) ou l’**Architecture Hexagonale**. Elle favorise la flexibilité en permettant de remplacer ou de modifier de grandes parties du code sans impacter le reste du système. Elle réduit également le couplage, rendant le système moins dépendant d’implémentations spécifiques. Cela simplifie les tests, car les modules peuvent être facilement simulés (*mockés*).

Pour toutes ces raisons, de nombreux développeurs, moi y compris, choisissent d’utiliser la DI dans les projets Python, notamment dans les API web.

Dans cet article, je vais partager la méthode que j’utilise pour implémenter l’**injection de dépendances** dans une API web typique construite avec FastAPI. Bien que FastAPI propose un système de DI intégré (que vous pouvez consulter [ici](https://fastapi.tiangolo.com/tutorial/dependencies/?ref=sfeir.dev)), je trouve sa syntaxe quelque peu lourde. De plus, injecter des dépendances dans les couches internes d’une architecture DDD (comme la couche *domaine*) n’est pas aussi simple que dans la couche API. La définition d’objets *singleton* peut également être complexe.

C’est pourquoi j’ai développé une technique que je trouve plus facile à suivre, à implémenter et à maintenir, et je suis ravi de la partager avec vous.

## Qu’est-ce que l’injection de dépendances ?

L’**injection de dépendances** est un patron de conception utilisé pour mettre en œuvre le principe d’**Inversion de Contrôle** (*inversion of control* ou **IoC**) Ce principe est le dernier des principes **SOLID** définis par **Robert C. Martin** (aussi connu comme « [Uncle Bob](https://www.sfeir.dev/back/robert-martin-aka-uncle-bob-le-pere-de-la-clean-architecture/) »), qui servent de guide pour écrire du code évolutif, maintenable et faiblement couplé (jetez un coup d'œil aux articles [Késaco : Les principes SOLID?](https://www.sfeir.dev/kesaco-les-principes-solid/) et [Les principes SOLID, pour quoi faire ?](https://www.sfeir.dev/les-principes-solid-pourquoi-faire/) de mon collègue Thibaut Rety pour connaître plus en profondeur ces principes).

La DI repose sur deux règles fondamentales :

1. « Les modules de **haut niveau** ne doivent pas dépendre des modules de **bas niveau**. Les deux doivent dépendre d’**abstractions**. »
2. « Les **abstractions** ne doivent pas dépendre des **détails**. Les **détails** doivent dépendre des **abstractions**. »

Décomposons ces règles :

### Règle 1 : Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau

Un **module de bas niveau** est un composant qui effectue des opérations de base et qui a peu ou pas de dépendances. Ces modules sont généralement réutilisables et autonomes. À l’inverse, les **modules de haut niveau** encapsulent la logique métier et dépendent souvent d’autres composants (comme des dépôts, des services ou des API externes) pour fonctionner.

Voici un exemple de code qui **viole** cette règle :

```python
class MyDatabase:
    ...
    def connect(self):
        print("Connexion à la base de données")
    ...

class UserService:
    def __init__(self):
        self.db = MyDatabase()  # <-- dépendance

    def get_user(self):
        self.db.connect()
        print("Récupération des données utilisateur")
    ...

def main():
    service = UserService()  # <-- dépendance
    service.get_user()
    ...

if __name__ == "__main__":
    main()

```

Dans cet exemple, `MyDatabase` est un **module de bas niveau**, et `UserService` est un **module de haut niveau**. La dépendance est créée directement dans `UserService`, ce qui signifie que si nous devons changer l’implémentation de `MyDatabase`, nous devrons probablement modifier toutes les classes qui l’utilisent—à commencer par `UserService`. Ce couplage fort rend le système difficile à maintenir et à faire évoluer.

De plus, il devient difficile de créer ou de simuler (`mock`) des instances de `UserService` sans s’assurer que `MyDatabase` fonctionne correctement. Cette chaîne de dépendances complique les tests et réduit la flexibilité.

Une meilleure approche consiste à dépendre d’une **abstraction**, comme une interface :

```python
from abc import ABC, abstractmethod

# Abstraction de haut niveau
class IDatabase(ABC):

    @abstractmethod
    def connect(self):
        raise NotImplementedError

# Implémentation de bas niveau
class MyDatabase(IDatabase):
    ...
    def connect(self):
        print("Connexion à la base MySQL")
    ...

# Module de haut niveau dépendant de l’abstraction
class UserService:
    def __init__(self, db: IDatabase):  # <-- dépendance de l'abstraction
        self.db = db

    def get_user(self):
        self.db.connect()
        print("Récupération des données utilisateur")
    ...

def main():
    db = MyDatabase()  # <-- dépendance
    service = UserService(db)  # <-- dépendance
    service.get_user()
    ...

if __name__ == "__main__":
    main()

```

Dans ce second exemple, le couplage fort est résolu. Une interface `IDatabase` est introduite, permettant à `UserService` d’être complètement détaché de l’implémentation concrète de `MyDatabase`. Désormais, toutes les dépendances sont injectées au niveau le plus élevé—généralement dans la fonction `main` ou lors de la configuration de l’application—tandis que les modules eux-mêmes ne dépendent que d’abstractions. Cette structure garantit que les modules sont **faiblement couplés**, respectant ainsi la **première règle** de l’injection de dépendances.

### Règle 2 : Les abstractions ne doivent pas dépendre des détails

Cette règle souligne qu’une abstraction doit définir **ce que** fait un composant, et non **comment** il le fait. Les détails d’implémentation doivent être encapsulés dans des classes concrètes, et non exposés ou gérés par l’abstraction elle-même.

Si une interface ou une classe abstraite commence à inclure de la logique ou des hypothèses sur des implémentations spécifiques, elle devient fortement couplée à ces détails. Cela viole le principe et **risque** de provoquer des **changements en cascade** dans tout le système dès qu’une seule implémentation change.

Voici un exemple pour illustrer cela :

```python
from abc import ABC, abstractmethod

class ITransportationMode(ABC):

    @abstractmethod
    def get_max_speed(self):
        raise NotImplementedError

    @abstractmethod
    def get_wheels_quantity(self):
        raise NotImplementedError

class Car(ITransportationMode):
    def get_max_speed(self):
        return 200

    def get_wheels_quantity(self):  # <-- fonctionne ici
        return 4

class Horse(ITransportationMode): 
    
    def get_max_speed(self):
        return 70

    def get_wheels_quantity(self):  # <-- ne fonctionne plus ici
        pass

```

Cet exemple met en évidence une erreur courante : la méthode `get_wheels_quantity` (obtenir la quantité de roues) est définie dans l’interface `ITransportationMode`, mais elle n’a de sens que pour certaines implémentations comme `Car` (voiture). Pour d’autres, comme `Horse` (cheval), elle est inutile ou non définie, ce qui conduit à des implémentations maladroites ou vides.

Cela viole la deuxième règle de l’injection de dépendances : **les abstractions ne doivent pas dépendre des détails**. L’interface ne doit définir que des comportements communs et pertinents pour toutes ses implémentations. Inclure des méthodes qui ne s’appliquent qu’à certaines sous-classes force un couplage inutile et casse l’abstraction.

Pour résoudre ce problème, nous pouvons diviser l’abstraction en interfaces plus spécifiques :

```python
from abc import ABC, abstractmethod

class ITransportationMode(ABC):
	@abstractmethod
	def get_max_speed(self):
		raise NotImplementedError
		
class IVehicleWithWheels(ITransportationMode):
	@abstractmethod
	def get_wheels_quantity(self):
		raise NotImplementedError

class Car(IVehicleWithWheels):
	def get_max_speed(self):
		return 200

	def get_wheels_quantity(self):
		return 4

class Horse(ITransportationMode): 
	def get_max_speed(self):
		return 70

```

Le nouveau code offre beaucoup plus de flexibilité, car lorsqu’une modification survient, elle n’affecte que les classes concernées. Cela améliore la **modularisation** et réduit le **couplage**. De plus, les responsabilités de chaque module sont clairement définies.

## Injection de dépendances en Python

Je vais maintenant vous présenter une API web simple pour illustrer ma méthodologie d’implémentation du principe d’**Inversion de Contrôle (IoC)** en Python. Vous pouvez consulter le projet complet sur GitHub [ici](https://github.com/uliseshdzc/fastapi-dependency-injection?ref=sfeir.dev).

Pour cela, j’utilise deux bibliothèques principales pour l’injection de dépendances, une pour la gestion des paramètres de l’application, et bien sûr, FastAPI :

| Type          | Bibliothèque      | Version utilisée |
| ------------- | ----------------- | ---------------- |
| DI            | FastAPI Injector  | 0.8.0            |
| DI            | Injector          | 0.22.0           |
| Configuration | Pydantic Settings | 2.10.1           |
| API           | FastAPI           | 0.116.1          |

L’application est une API simple qui retourne les propriétés de différents polygones. Elle est structurée selon les principes du **Domain-Driven Design (DDD)** :

```
fastapi-dependency-injection
│
├── polygons/
│	│
│	├── api/
│	│   ├── controllers/
|	│   │   └── polygons.py
|	│   ├── app.py
|	│   └── dependency_injection.py
|	│   
|	├── domain/
|	│   ├── interfaces/
|	│   │   ├── polygon_interface.py
|	│   │   └── settings_interface.py
|	│   ├── models/
|	│   │   ├── square.py
|	│   │   └── triangle.py
|	│   └── settings/
|	|
|	├── __init__.py
|	└── __main__.py
|
├── .env
└── requirements.txt

```

### Injection de la configuration

D’après mon expérience, l’un des principaux avantages de l’injection de dépendances est la possibilité d’injecter les **paramètres de l’application**. En général, une classe de configuration est utilisée dans plusieurs modules. Sans DI, il faudrait l’instancier à chaque fois qu’elle est nécessaire, ce qui conduit à un code répétitif et moins maintenable.

De plus, les valeurs de configuration—comme les paramètres d’environnement—doivent rester constantes tout au long du cycle de vie de l’application. Cela en fait des candidats idéaux pour le **patron singleton**.

Heureusement, cela est facile à mettre en œuvre grâce à `pydantic` et `injector`. Commençons par créer un fichier `settings.py` qui lit la longueur des côtés du polygone à partir d’un fichier `.env`.

```
SIDE_LENGTH=5

```

.env

```python
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")
    side_length: float

```

settings.py

Voici maintenant la partie intéressante. Pour appliquer l'**inversion de contrôle**, nous avons besoin d'une interface qui agit comme un contrat entre les modules nécessitant des paramètres et l'implémentation des paramètres elle-même. Cette interface garantit que toute classe qui en hérite doit implémenter la méthode `get_side_length` . Ainsi, tout objet utilisant `ISettings` peut compter sur la présence de cette méthode.

```python
from abc import ABC, abstractmethod

class ISettings(ABC):

    @abstractmethod
    def get_side_length(self) -> float:
        raise NotImplementedError

```

settings\_interface.py

Il s'agit de la déclaration finale de la classe `Settings`. Elle hérite de `ISettings` et implémente la méthode `get_side_length`. Notez l'utilisation du décorateur `@singleton` de la bibliothèque `injector` : cela garantit qu'une seule instance de la classe `Settings` est créée et partagée dans toute l'application, conformément au modèle **singleton**.

```python
from injector import singleton
from pydantic_settings import BaseSettings, SettingsConfigDict

from polygons.domain.interfaces.settings_interface import ISettings
  
@singleton
class Settings(BaseSettings, ISettings):
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")
    side_length: float

    def get_side_length(self) -> float:
        return self.side_length

```

settings.py

### Polymorphisme

Ensuite, définissons l’interface pour les polygones. Cette interface inclut trois méthodes qui décrivent le comportement attendu de toute implémentation de polygone.

```python
from abc import ABC, abstractmethod

class IPolygon(ABC):

    @abstractmethod
    def get_area(self, side_length: float) -> float:
        raise NotImplementedError

    @abstractmethod
    def get_perimeter(self, side_length: float) -> float:
        raise NotImplementedError

    @abstractmethod
    def get_number_of_sides(self) -> int:
        raise NotImplementedError

```

polygons\_interface.py

Implémentons maintenant une classe qui hérite de `IPolygon`. La classe `Square` fournit des implémentations concrètes pour les trois méthodes. Elle nécessite un accès au paramètre `side_length`, qui est injecté via le constructeur en utilisant l’interface `ISettings`. Sans **injection de dépendances**, nous serions obligés d’instancier manuellement la classe de configuration à chaque fois qu’elle est nécessaire, ce qui rend le code répétitif et complique la gestion des modifications de configuration à travers toute l’application. Par exemple, imaginez que nous décidions de changer la valeur de `side_length` de 5 à 10 ; il faudrait alors mettre à jour chaque endroit où `Settings` est instancié manuellement. En injectant `ISettings`, nous déléguons la responsabilité de fournir la dépendance à l’injecteur.

❗

Il est nécessaire d'utiliser le décorateur `@inject` pour indiquer à la bibliothèque `injector` que la classe doit être injectée.

```python
from injector import inject

from polygons.domain.interfaces.polygon_interface import IPolygon
from polygons.domain.interfaces.settings_interface import ISettings

class Square(IPolygon):

    @inject
    def __init__(self, settings: ISettings):
		# Si les paramètres n'étaient pas injectés, je devrais créer une nouvelle instance
		# self.settings = Settings(side_length=5)    
        self.settings = settings  

    def get_area(self) -> float:
        return self.settings.get_side_length() ** 2

    def get_perimeter(self) -> float:
        return 4 * self.settings.get_side_length()

    def get_number_of_sides(self) -> int:
        return 4

```

square.py

Une implémentation similaire est fournie pour la classe `Triangle` :

```python
from injector import inject

from polygons.domain.interfaces.polygon_interface import IPolygon
from polygons.domain.interfaces.settings_interface import ISettings

class Triangle(IPolygon):

    @inject
    def __init__(self, settings: ISettings):
        self.settings = settings

    def get_area(self) -> float:
        return 0.5 * self.settings.get_side_length() ** 2

    def get_perimeter(self) -> float:
        return 3 * self.settings.get_side_length()

    def get_number_of_sides(self) -> int:
        return 3

```

triangle.py

### Liaison des classes

Pour finaliser la configuration, nous devons définir comment les interfaces sont liées à leurs implémentations. Cela se fait dans le fichier `dependency_injection.py`, situé à la racine de l’application. Grâce à la bibliothèque `injector`, le processus de liaison est simple :

1. Créer une instance `injector = Injector()`.
2. Lier les interfaces à leurs classes concrètes.
3. Définir éventuellement une [portée](https://injector.readthedocs.io/en/latest/scopes.html?ref=sfeir.dev), comme `singleton`, soit via des décorateurs, soit directement dans le *binder*.

```python
import random  

from injector import Injector

from polygons.domain.interfaces.polygon_interface import IPolygon
from polygons.domain.interfaces.settings_interface import ISettings
from polygons.domain.models.square import Square
from polygons.domain.models.triangle import Triangle
from polygons.domain.settings.settings import Settings

injector = Injector()
injector.binder.bind(IPolygon, random.choice([Square, Triangle]))
injector.binder.bind(ISettings, Settings)

```

dependency\_injection.py

Cette configuration lie `ISettings` à la classe `Settings`, garantissant que toute classe nécessitant `ISettings` reçoit l’instance unique (singleton). À des fins de démonstration, `IPolygon` est lié aléatoirement à `Square` ou `Triangle`, illustrant à quel point l’injection de dépendances peut être **flexible et dynamique**—même si ce type de liaison aléatoire n’est pas courant en environnement de production.

### Injection avec FastAPI

J’ai créé une application FastAPI simple avec trois points de terminaison : `/random-polygon`, `/square` et `/triangle`.

```python
import uvicorn
  
def main():
    uvicorn.run("polygons.api.app:app")
  
if __name__ == "__main__":
    main()

```

\_\_main\_\_.py

L’une des étapes clés consiste à intégrer l’injecteur à l’objet `app` de FastAPI. À première vue, le fichier `app.py` ressemble à une configuration FastAPI classique. Cependant, la dernière ligne est cruciale : la fonction `attach_injector` de la bibliothèque `fastapi_injector` permet de **lier l’injecteur** à l’application **FastAPI**.

```python
from fastapi import FastAPI
from fastapi_injector import attach_injector
  
from polygons.api.controllers import polygons
from polygons.api.dependency_injection import injector

app = FastAPI(
    title="Polygons DI example",
    version="EXAMPLE",
    description="API conceived to show how to implement dependency injection (DI) in Python using FastAPI.",
    swagger_ui_parameters={"defaultModelsExpandDepth": -1},
)
  
app.include_router(polygons.router)
attach_injector(app, injector)

```

app.py

Pour le fichier du contrôleur, je me concentre désormais uniquement sur l’injection des services nécessaires dans les points de terminaison. Nous ne nous soucions plus de l’implémentation des autres dépendances, car elles sont toutes gérées par l’injecteur (c’est la beauté du **principe d’inversion de dépendance** ).

Avec `fastapi_injector`, la syntaxe pour injecter des dépendances change légèrement par rapport à l’approche traditionnelle de FastAPI.

#### Injection traditionnelle avec FastAPI :

```python
...
from typing import Annotated
from fastapi import Depends

@router.get("/example")
def my_endpoint(
	polygon_service: Annotated[IPolygon, Depends(Square)]
):
	...

```

Dans cette approche, vous devez définir explicitement la liaison pour chaque interface à chaque point de terminaison.

#### Avec `fastapi_injector` :

```python
...
from fastapi_injector import Injected

@router.get("/example")
def my_endpoint(
	polygon_service: IPolygon = Injected(IPolygon)
):

```

Ici, la liaison est automatiquement résolue en fonction de la configuration définie dans `dependency_injection.py`. Cependant, vous pouvez toujours **remplacer la liaison par défaut** en spécifiant une classe concrète, comme illustré dans l’implémentation du contrôleur.

### Implémentation du contrôleur

```python
from fastapi import APIRouter
from fastapi_injector import Injected
  
from polygons.domain.interfaces.polygon_interface import IPolygon
from polygons.domain.models.square import Square
from polygons.domain.models.triangle import Triangle

router = APIRouter(tags=["Polygons"])

@router.get("/random-polygon")
def get_random_polygon_information(polygon_service: IPolygon = Injected(IPolygon)):
    return {
        "sides": polygon_service.get_number_of_sides(),
        "area": polygon_service.get_area(),
        "perimeter": polygon_service.get_perimeter(),
    }
  
@router.get("/square")
def get_square_information(polygon_service: IPolygon = Injected(Square)):
    return {
        "sides": polygon_service.get_number_of_sides(),
        "area": polygon_service.get_area(),
        "perimeter": polygon_service.get_perimeter(),
    }
  
@router.get("/triangle")
def get_triangle_information(polygon_service: IPolygon = Injected(Triangle)):
    return {
        "sides": polygon_service.get_number_of_sides(),
        "area": polygon_service.get_area(),
        "perimeter": polygon_service.get_perimeter(),
    }

```

controllers/polygons.py

Pour le point de terminaison `/random-polygon`, l’implémentation utilisée sera sélectionnée aléatoirement entre `Square` et `Triangle`, selon la liaison définie dans `dependency_injection.py` :

```python
injector.binder.bind(IPolygon, random.choice([Square, Triangle]))

```

Pour les points de terminaison `/square` et `/triangle`, nous spécifions explicitement quelle implémentation injecter.

Et voilà ! Nous avons construit une application FastAPI avec une **injection de dépendances** propre et maintenable, en utilisant `injector` et `fastapi_injector`.

## Conclusion

Le **principe d’inversion de dépendance** (*dependency inversion principle* ou **DIP** en anglais) offre des avantages considérables en matière de **modularisation** et de **réduction du couplage**. Bien que Python soit un langage dynamiquement typé, appliquer le DIP peut grandement améliorer la structure et la maintenabilité des applications complexes. Il favorise l’évolutivité et simplifie les tests en permettant de remplacer ou de simuler facilement les composants.

Dans cet article, j’ai présenté une méthodologie simple pour implémenter l’injection de dépendances dans une application FastAPI en utilisant les bibliothèques `injector` et `fastapi_injector`. Cette approche est particulièrement adaptée aux projets plus grands et complexes, et constitue une base solide pour un développement Python propre et évolutif.

Je vous encourage à mettre en pratique les principes **SOLID**—en particulier le **DIP**—afin d’améliorer la qualité, la flexibilité et la testabilité de votre code.