Iniezione di dipendenze: un modo Python
Pattern DI in Python per codice pulito e testabile
Iniezione di dipendenze (DI) è un modello di progettazione fondamentale che promuove codice pulito, testabile e mantenibile in applicazioni Python.
Che tu stia costruendo API REST con FastAPI, implementando test unitari o lavorando con funzioni AWS Lambda, comprendere l’iniezione di dipendenze migliorerà significativamente la qualità del tuo codice.

Cosa è l’iniezione di dipendenze?
L’iniezione di dipendenze è un modello di progettazione in cui i componenti ricevono le loro dipendenze da fonti esterne piuttosto che crearle internamente. Questo approccio decoppia i componenti, rendendo il tuo codice più modulare, testabile e mantenibile.
In Python, l’iniezione di dipendenze è particolarmente potente grazie alla natura dinamica del linguaggio e al supporto per i protocolli, le classi base astratte e il duck typing. La flessibilità di Python significa che puoi implementare i pattern DI senza framework pesanti, sebbene siano disponibili framework quando necessari.
Perché utilizzare l’iniezione di dipendenze in Python?
Migliore testabilità: Iniettando le dipendenze, puoi facilmente sostituire le implementazioni reali con mock o test double. Questo ti permette di scrivere test unitari veloci, isolati e che non richiedono servizi esterni come database o API. Quando scrivi test unitari completi, l’iniezione di dipendenze rende banale sostituire le dipendenze reali con test double.
Migliore manutenibilità: Le dipendenze diventano esplicite nel tuo codice. Quando guardi un costruttore, vedi immediatamente cosa richiede un componente. Questo rende il codicebase più facile da comprendere e modificare.
Decoupling: I componenti dipendono da astrazioni (protocolli o ABC) piuttosto che da implementazioni concrete. Questo significa che puoi cambiare le implementazioni senza influenzare il codice dipendente.
Flessibilità: Puoi configurare diverse implementazioni per diversi ambienti (sviluppo, test, produzione) senza modificare la logica aziendale. Questo è particolarmente utile quando si distribuiscono applicazioni Python su diversi piattaforme, che siano AWS Lambda o server tradizionali.
Iniezione di costruttore: Il modo Python
Il modo più comune e idiomatico per implementare l’iniezione di dipendenze in Python è attraverso l’iniezione di costruttore — accettando le dipendenze come parametri nel metodo __init__.
Esempio base
Ecco un esempio semplice che dimostra l’iniezione di costruttore:
from typing import Protocol
from abc import ABC, abstractmethod
# Definizione di un protocollo per il repository
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Il servizio dipende dal protocollo del repository
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)
Questo modello rende chiaro che UserService richiede un UserRepository. Non puoi creare un UserService senza fornire un repository, il che prevene errori di runtime da dipendenze mancanti.
Multiple Dependencies
Quando un componente ha più dipendenze, aggiungile semplicemente come parametri del costruttore:
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
Utilizzo di Protocol e Classi Base Astratte
Uno dei principi chiave nell’implementare l’iniezione di dipendenze è il Principio di Inversione delle Dipendenze (DIP): i moduli di alto livello non devono dipendere da moduli di basso livello; entrambi devono dipendere da astrazioni.
In Python, puoi definire astrazioni utilizzando Protocol (typing strutturale) o Classi Base Astratte (ABC) (typing nominale).
Protocol (Python 3.8+)
I Protocol utilizzano il typing strutturale — se un oggetto ha i metodi richiesti, soddisfa il protocollo:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Qualsiasi classe con il metodo process_payment soddisfa questo protocollo
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Logica per carta di credito
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Logica per PayPal
return True
# Il servizio accetta qualsiasi PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Classi Base Astratte
Le ABC utilizzano il typing nominale — le classi devono ereditare esplicitamente dall’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
Quando utilizzare Protocol vs ABC: Utilizza Protocol quando desideri typing strutturale e flessibilità. Utilizza ABC quando devi imporre gerarchie di ereditarietà o fornire implementazioni predefinite.
Esempio reale: Astrazione del database
Quando lavori con database in applicazioni Python, spesso devi astrarre le operazioni del database. Ecco come l’iniezione di dipendenze aiuta:
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]:
...
# Il repository dipende dall'astrazione
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
Questo modello ti permette di sostituire le implementazioni del database (PostgreSQL, SQLite, MongoDB) senza modificare il codice del repository.
Il Pattern Root di Composizione
Il Root di Composizione è il punto in cui assembli tutte le dipendenze all’ingresso dell’applicazione (tipicamente main.py o il factory dell’applicazione). Questo centralizza la configurazione delle dipendenze e rende il grafico delle dipendenze esplicito.
def create_app() -> FastAPI:
app = FastAPI()
# Inizializza le dipendenze dell'infrastruttura
db = init_database()
logger = init_logger()
# Inizializza i repository
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Inizializza i servizi con le dipendenze
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Inizializza i gestori HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Collega le route
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Questo approccio rende chiaro come è strutturata l’applicazione e da dove provengono le dipendenze. È particolarmente utile quando si costruiscono applicazioni seguendo principi di architettura pulita, dove devi coordinare diversi strati di dipendenze.
Framework di Iniezione di Dipendenze
Per applicazioni più grandi con grafici complessi di dipendenze, gestire le dipendenze manualmente può diventare oneroso. Python ha diversi framework DI che possono aiutare:
Dependency Injector
Dependency Injector è un framework popolare che fornisce un approccio basato su container per l’iniezione di dipendenze.
Installazione:
pip install dependency-injector
Esempio:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Configurazione
config = providers.Configuration()
# Database
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Repository
user_repository = providers.Factory(
UserRepository,
db=db
)
# Servizi
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Utilizzo
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector è una libreria leggera ispirata a Guice di Google, che si concentra sulla semplicità.
Installazione:
pip install injector
Esempio:
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)
Quando utilizzare i framework
Utilizza un framework quando:
- Il grafico delle dipendenze è complesso con molti componenti interdipendenti
- Hai molte implementazioni dell’interfaccia che devono essere selezionate in base alla configurazione
- Vuoi una risoluzione automatica delle dipendenze
- Stai costruendo un’applicazione grande dove l’incatenamento manuale diventa error-prone
Mantieni l’iniezione manuale quando:
- L’applicazione è piccola o di media dimensione
- Il grafico delle dipendenze è semplice e facile da seguire
- Vuoi mantenere le dipendenze minimali e esplicite
- Preferisci il codice esplicito rispetto alla magia del framework
Testing con Iniezione di Dipendenze
Uno dei principali vantaggi dell’iniezione di dipendenze è la migliorata testabilità. Ecco come DI rende più facile il testing:
Esempio di test unitario
from unittest.mock import Mock
import pytest
# Implementazione mock per il testing
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 utilizzando il 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"
Questo test esegue velocemente, non richiede un database e testa la tua logica aziendale in isolamento. Quando lavori con test unitari in Python, l’iniezione di dipendenze rende facile creare test double e verificare le interazioni.
Utilizzo di fixture pytest
Le fixture pytest funzionano ottimamente con l’iniezione di dipendenze:
@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"
Pattern comuni e buone pratiche
1. Utilizza la segregazione delle interfacce
Mantieni i protocolli e le interfacce piccoli e focalizzati su ciò che il client effettivamente necessita:
# Buono: Il client necessita solo di leggere gli utenti
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Interfaccia separata per la scrittura
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Valida le dipendenze nei costruttori
I costruttori devono validare le dipendenze e sollevare errori chiari se l’inizializzazione fallisce:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("user repository cannot be None")
self.repo = repo
3. Utilizza gli hint di tipo
Gli hint di tipo rendono le dipendenze esplicite e aiutano con il supporto dell’IDE e il controllo del tipo statico:
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. Evita l’iniezione eccessiva
Non iniettare dipendenze che sono veramente dettagli interni di implementazione. Se un componente crea e gestisce i propri oggetti helper, va bene:
# Buono: L'helper interno non necessita dell'iniezione
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Cache interna - non necessita dell'iniezione
self._cache: dict[int, User] = {}
5. Documenta le dipendenze
Utilizza le docstring per documentare il motivo per cui sono necessarie le dipendenze e eventuali vincoli:
class UserService:
"""UserService gestisce la logica aziendale relativa agli utenti.
Args:
repo: UserRepository per l'accesso ai dati. Deve essere thread-safe
se utilizzato in contesti concorrenti.
logger: Logger per il tracciamento degli errori e il debug.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Iniezione di dipendenze con FastAPI
FastAPI ha un supporto integrato per l’iniezione di dipendenze attraverso il meccanismo 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
Il sistema di iniezione di dipendenze di FastAPI gestisce automaticamente il grafico delle dipendenze, rendendo facile costruire API pulite e mantenibili.
Quando NON utilizzare l’iniezione di dipendenze
L’iniezione di dipendenze è uno strumento potente, ma non è sempre necessaria:
Saltare l’iniezione di dipendenze per:
- Oggetti semplici o classi dati
- Funzioni interne o utilità
- Script unici o piccole utilità
- Quando l’istanziazione diretta è più chiara e semplice
Esempio di quando NON utilizzare l’iniezione di dipendenze:
# Classe dati semplice - non necessita di DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Utilità semplice - non necessita di DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integrazione con l’ecosistema Python
L’iniezione di dipendenze funziona in modo impeccabile con altri pattern e strumenti Python. Quando si costruiscono applicazioni che utilizzano pacchetti Python o framework di test unitari, puoi iniettare questi servizi nella tua logica aziendale:
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
Questo ti permette di sostituire le implementazioni o utilizzare mock durante i test.
Iniezione di dipendenze asincrona
La sintassi async/await di Python funziona bene con l’iniezione di dipendenze:
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]
Conclusione
L’iniezione di dipendenze è un pilastro per scrivere codice Python mantenibile e testabile. Seguendo i pattern descritti in questo articolo — iniezione di costruttore, progettazione basata su protocollo e il pattern root di composizione — creerai applicazioni più facili da comprendere, testare e modificare.
Inizia con l’iniezione manuale di costruttore per applicazioni piccole o di media dimensione, e considera framework come dependency-injector o injector quando il grafico delle dipendenze cresce. Ricorda che l’obiettivo è chiarezza e testabilità, non complessità per il proprio conto.
Per ulteriori risorse di sviluppo Python, consulta il nostro Python Cheatsheet per un riferimento rapido sulla sintassi Python e i pattern comuni.
Link utili
- Python Cheatsheet
- Unit Testing in Python
- Python Design Patterns for Clean Architecture
- Structured Output - LLMs on Ollama with Qwen3 - Python and Go
- Structured output comparison across popular LLM providers - OpenAI, Gemini, Anthropic, Mistral and AWS Bedrock
- Building a Dual-Mode AWS Lambda with Python and Terraform
Risorse esterne
- Dependency Injection in Python - Real Python
- How Dependency Injection in Python Improves Code Structure - Volito Digital
- Python Dependency Injection Tutorial - DataCamp
- Dependency Injector - Official Documentation
- Injector - Lightweight DI Framework
- FastAPI Dependencies - Official Docs
- SOLID Principles in Python - Software Patterns Lexicon
- Python Protocols and Structural Subtyping - PEP 544