Inyección de dependencias: una manera en Python
Patrones de DI en Python para código limpio y fácil de probar
Inyección de dependencias (DI) es un patrón de diseño fundamental que promueve código limpio, testable y mantenible en aplicaciones Python.
Ya sea que estés construyendo APIs REST con FastAPI, implementando pruebas unitarias o trabajando con funciones de AWS Lambda, entender la inyección de dependencias mejorará significativamente la calidad de tu código.

¿Qué es la inyección de dependencias?
La inyección de dependencias es un patrón de diseño donde los componentes reciben sus dependencias de fuentes externas en lugar de crearlas internamente. Este enfoque desacopla componentes, haciendo que tu código sea más modular, testable y mantenible.
En Python, la inyección de dependencias es especialmente poderosa debido a la naturaleza dinámica del lenguaje y su soporte para protocolos, clases base abstractas y tipado duck. La flexibilidad de Python significa que puedes implementar patrones de DI sin frameworks pesados, aunque están disponibles cuando se necesiten.
¿Por qué usar inyección de dependencias en Python?
Mejora de la testabilidad: Al inyectar dependencias, puedes reemplazar fácilmente las implementaciones reales con mocks o dobles de prueba. Esto permite escribir pruebas unitarias que son rápidas, aisladas y no requieren servicios externos como bases de datos o APIs. Cuando escribes pruebas unitarias completas, la inyección de dependencias hace trivial reemplazar dependencias reales con dobles de prueba.
Mejor mantenibilidad: Las dependencias se vuelven explícitas en tu código. Cuando miras un constructor, inmediatamente ves qué requiere un componente. Esto hace que la base de código sea más fácil de entender y modificar.
Desacoplamiento: Los componentes dependen de abstracciones (protocolos o ABCs) en lugar de implementaciones concretas. Esto significa que puedes cambiar implementaciones sin afectar el código dependiente.
Flexibilidad: Puedes configurar diferentes implementaciones para diferentes entornos (desarrollo, prueba, producción) sin cambiar tu lógica de negocio. Esto es especialmente útil cuando se despliegan aplicaciones Python en diferentes plataformas, ya sea AWS Lambda o servidores tradicionales.
Inyección por constructor: La forma Python
La forma más común e idiomática de implementar la inyección de dependencias en Python es a través de la inyección por constructor—aceptando dependencias como parámetros en el método __init__.
Ejemplo básico
Aquí hay un ejemplo simple que demuestra la inyección por constructor:
from typing import Protocol
from abc import ABC, abstractmethod
# Definir un protocolo para el repositorio
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# El servicio depende del protocolo del repositorio
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)
Este patrón hace claro que UserService requiere un UserRepository. No puedes crear un UserService sin proporcionar un repositorio, lo que previene errores de tiempo de ejecución por dependencias faltantes.
Múltiples dependencias
Cuando un componente tiene múltiples dependencias, simplemente agrégalas como parámetros del constructor:
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
Usando protocolos y clases base abstractas
Uno de los principios clave al implementar la inyección de dependencias es el Principio de Inversión de Dependencias (DIP): los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de abstracciones.
En Python, puedes definir abstracciones usando Protocolos (tipado estructural) o Clases Base Abstractas (ABCs) (tipado nominal).
Protocolos (Python 3.8+)
Los protocolos usan tipado estructural—si un objeto tiene los métodos requeridos, satisface el protocolo:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Cualquier clase con el método process_payment satisface este protocolo
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Lógica de tarjeta de crédito
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Lógica de PayPal
return True
# El servicio acepta cualquier PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Clases Base Abstractas
Las ABCs usan tipado nominal—las clases deben heredar explícitamente de la 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
Cuándo usar protocolos vs ABCs: Usa protocolos cuando quieras tipado estructural y flexibilidad. Usa ABCs cuando necesites imponer jerarquías de herencia o proporcionar implementaciones por defecto.
Ejemplo real: Abstracción de base de datos
Cuando trabajas con bases de datos en aplicaciones Python, a menudo necesitas abstraer operaciones de base de datos. Aquí es cómo la inyección de dependencias ayuda:
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]:
...
# El repositorio depende de la abstracción
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
Este patrón permite cambiar implementaciones de base de datos (PostgreSQL, SQLite, MongoDB) sin cambiar tu código del repositorio.
El patrón raíz de composición
El patrón raíz de composición es donde se ensamblan todas tus dependencias en el punto de entrada de la aplicación (normalmente main.py o tu fábrica de aplicación). Esto centraliza la configuración de dependencias y hace explícito el gráfico de dependencias.
def create_app() -> FastAPI:
app = FastAPI()
# Inicializar dependencias de infraestructura
db = init_database()
logger = init_logger()
# Inicializar repositorios
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Inicializar servicios con dependencias
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Inicializar manejadores HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Conectar rutas
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Este enfoque hace claro cómo está estructurada tu aplicación y de dónde provienen las dependencias. Es especialmente valioso cuando se construyen aplicaciones siguiendo principios de arquitectura limpia, donde necesitas coordinar múltiples capas de dependencias.
Frameworks de inyección de dependencias
Para aplicaciones más grandes con gráficos complejos de dependencias, gestionar dependencias manualmente puede volverse incómodo. Python tiene varios frameworks de DI que pueden ayudar:
Dependency Injector
Dependency Injector es un framework popular que proporciona un enfoque basado en contenedores para la inyección de dependencias.
Instalación:
pip install dependency-injector
Ejemplo:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Configuración
config = providers.Configuration()
# Base de datos
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Repositorios
user_repository = providers.Factory(
UserRepository,
db=db
)
# Servicios
user_service = providers.Factory(
UserService,
repo=user_repository
)
# Uso
container = Container()
container.config.database.url.from_env("DATABASE_URL")
user_service = container.user_service()
Injector
Injector es una biblioteca liviana inspirada en Guice de Google, enfocada en simplicidad.
Instalación:
pip install injector
Ejemplo:
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)
Cuándo usar frameworks
Usa un framework cuando:
- Tu gráfico de dependencias es complejo con muchos componentes interdependientes
- Tienes múltiples implementaciones de la misma interfaz que necesitan seleccionarse según la configuración
- Quieres resolución automática de dependencias
- Estás construyendo una aplicación grande donde el cableado manual se vuelve propenso a errores
Usa DI manual cuando:
- Tu aplicación es pequeña a mediana
- El gráfico de dependencias es simple y fácil de seguir
- Quieres mantener las dependencias mínimas y explícitas
- Prefieres código explícito sobre magia de framework
Pruebas con inyección de dependencias
Uno de los principales beneficios de la inyección de dependencias es la mejora de la testabilidad. Aquí es cómo DI hace más fácil las pruebas:
Ejemplo de prueba unitaria
from unittest.mock import Mock
import pytest
# Implementación de prueba para pruebas
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
# Prueba usando el 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"
Esta prueba se ejecuta rápidamente, no requiere una base de datos y prueba tu lógica de negocio en aislamiento. Cuando trabajas con pruebas unitarias en Python, la inyección de dependencias hace fácil crear dobles de prueba y verificar interacciones.
Usando fixtures de pytest
Los fixtures de pytest funcionan excelente con la inyección de dependencias:
@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"
Patrones comunes y buenas prácticas
1. Usar segregación de interfaces
Mantén los protocolos e interfaces pequeños y enfocados en lo que el cliente realmente necesita:
# Bueno: El cliente solo necesita leer usuarios
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Interfaz separada para escritura
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Validar dependencias en constructores
Los constructores deben validar dependencias y lanzar errores claros si falla la inicialización:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("el repositorio de usuarios no puede ser None")
self.repo = repo
3. Usar anotaciones de tipo
Las anotaciones de tipo hacen explícitas las dependencias y ayudan con el soporte de IDE y la verificación de tipo estático:
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. Evitar la sobreinyección
No inyectes dependencias que sean verdaderamente detalles de implementación interna. Si un componente crea y gestiona sus propios objetos ayudantes, está bien:
# Bueno: El helper interno no necesita inyección
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Caché interno - no necesita inyección
self._cache: dict[int, User] = {}
5. Documentar dependencias
Usa docstrings para documentar por qué se necesitan las dependencias y cualquier restricción:
class UserService:
"""UserService maneja lógica de negocio relacionada con usuarios.
Args:
repo: UserRepository para acceso a datos. Debe ser seguro para hilos
si se usa en contextos concurrentes.
logger: Logger para seguimiento de errores y depuración.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Inyección de dependencias con FastAPI
FastAPI tiene soporte integrado para la inyección de dependencias a través de su mecanismo 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
El sistema de inyección de dependencias de FastAPI maneja automáticamente el gráfico de dependencias, lo que hace fácil construir APIs limpias y mantenibles.
Cuándo NO usar inyección de dependencias
La inyección de dependencias es una herramienta poderosa, pero no siempre es necesaria:
Omitir DI cuando:
- Son objetos de valor simples o clases de datos
- Son funciones o utilidades internas
- Son scripts o utilidades pequeños de uso único
- Cuando la instanciación directa es más clara y simple
Ejemplo de cuando NO usar DI:
# Clase de datos simple - no se necesita DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Utilidad simple - no se necesita DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integración con el ecosistema de Python
La inyección de dependencias funciona de forma sinérgica con otros patrones y herramientas de Python. Cuando construyes aplicaciones que usan paquetes Python o frameworks de pruebas unitarias, puedes inyectar estos servicios en tu lógica de negocio:
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"Generado informe {report_id}")
return pdf
Esto permite intercambiar implementaciones o usar mocks durante las pruebas.
Inyección de dependencias asincrónica
La sintaxis async/await de Python funciona bien con la inyección de dependencias:
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]
Conclusión
La inyección de dependencias es una piedra angular para escribir código Python mantenible y testable. Siguiendo los patrones descritos en este artículo—inyección por constructor, diseño basado en protocolos y el patrón raíz de composición—creará aplicaciones más fáciles de entender, probar y modificar.
Empieza con la inyección manual por constructor para aplicaciones pequeñas a medianas, y considera frameworks como dependency-injector o injector cuando tu gráfico de dependencias crezca. Recuerda que el objetivo es claridad y testabilidad, no complejidad por sí misma.
Para más recursos de desarrollo en Python, consulta nuestro Hoja de trucos de Python para referencias rápidas sobre sintaxis de Python y patrones comunes.
Enlaces útiles
- Hoja de trucos de Python
- Pruebas unitarias en Python
- Patrones de diseño en Python para arquitectura limpia
- Salida estructurada - LLMs en Ollama con Qwen3 - Python y Go
- Comparación de salida estructurada en proveedores populares de LLM - OpenAI, Gemini, Anthropic, Mistral y AWS Bedrock
- Construir una función AWS Lambda dual con Python y Terraform
Recursos externos
- Inyección de dependencias en Python - Real Python
- Cómo la inyección de dependencias en Python mejora la estructura del código - Volito Digital
- Tutorial de inyección de dependencias en Python - DataCamp
- Dependency Injector - Documentación oficial
- Injector - Framework de DI liviano
- Dependencias en FastAPI - Documentación oficial
- Principios SOLID en Python - Lexicon de patrones de software
- Protocolos y tipado estructural en Python - PEP 544