Injeção de Dependência: Uma Abordagem Python
Padrões de DI em Python para código limpo e testável
Injeção de dependência (DI) é um padrão de design fundamental que promove código limpo, testável e mantível em aplicações Python.
Seja você construindo APIs REST com FastAPI, implementando testes unitários, ou trabalhando com funções AWS Lambda, entender a injeção de dependência melhorará significativamente a qualidade do seu código.

O que é Injeção de Dependência?
A injeção de dependência é um padrão de design onde os componentes recebem suas dependências de fontes externas, em vez de criá-las internamente. Essa abordagem desacopla componentes, tornando seu código mais modular, testável e mantível.
Em Python, a injeção de dependência é particularmente poderosa devido à natureza dinâmica da linguagem e ao suporte para protocolos, classes base abstratas e tipagem pato. A flexibilidade do Python significa que você pode implementar padrões de DI sem frameworks pesados, embora frameworks estejam disponíveis quando necessários.
Por que usar injeção de dependência em Python?
Melhor testabilidade: Ao injetar dependências, você pode facilmente substituir implementações reais por mocks ou duplos de teste. Isso permite que você escreva testes unitários que são rápidos, isolados e não requerem serviços externos como bancos de dados ou APIs. Ao escrever testes unitários abrangentes, a injeção de dependência torna trivial trocar dependências reais por duplos de teste.
Melhor manutenibilidade: As dependências tornam-se explícitas no seu código. Quando você olha para um construtor, você imediatamente vê o que um componente requer. Isso torna o código mais fácil de entender e modificar.
Desacoplamento: Os componentes dependem de abstrações (protocolos ou ABCs) em vez de implementações concretas. Isso significa que você pode mudar implementações sem afetar o código dependente.
Flexibilidade: Você pode configurar diferentes implementações para diferentes ambientes (desenvolvimento, teste, produção) sem alterar sua lógica de negócios. Isso é especialmente útil ao implantar aplicações Python em diferentes plataformas, sejam AWS Lambda ou servidores tradicionais.
Injeção por Construtor: O jeito Python
A forma mais comum e idiomática de implementar injeção de dependência em Python é através da injeção por construtor — aceitando dependências como parâmetros no método __init__.
Exemplo Básico
Aqui está um exemplo simples demonstrando a injeção por construtor:
from typing import Protocol
from abc import ABC, abstractmethod
# Definindo um protocolo para o repositório
class UserRepository(Protocol):
def find_by_id(self, user_id: int) -> 'User | None':
...
def save(self, user: 'User') -> 'User':
...
# Serviço depende do protocolo do repositório
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)
Esse padrão torna claro que UserService requer um UserRepository. Você não pode criar um UserService sem fornecer um repositório, o que evita erros de tempo de execução devido a dependências ausentes.
Múltiplas Dependências
Quando um componente tem múltiplas dependências, basta adicioná-las como parâmetros do construtor:
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 e Classes Base Abstratas
Um dos princípios principais ao implementar injeção de dependência é o Princípio da Inversão de Dependência (DIP): módulos de alto nível não devem depender de módulos de baixo nível; ambos devem depender de abstrações.
Em Python, você pode definir abstrações usando Protocolos (tipagem estrutural) ou Classes Base Abstratas (ABCs) (tipagem nominal).
Protocolos (Python 3.8+)
Protocolos usam tipagem estrutural — se um objeto tiver os métodos necessários, ele satisfaz o protocolo:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
# Qualquer classe com o método process_payment satisfaz esse protocolo
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Lógica de cartão de crédito
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Lógica do PayPal
return True
# Serviço aceita qualquer PaymentProcessor
class OrderService:
def __init__(self, payment_processor: PaymentProcessor):
self.payment_processor = payment_processor
Classes Base Abstratas
ABCs usam tipagem nominal — classes devem herdar explicitamente da 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 usar Protocolos vs ABCs: Use Protocolos quando quiser tipagem estrutural e flexibilidade. Use ABCs quando precisar impor hierarquias de herança ou fornecer implementações padrão.
Exemplo Real: Abstração de Banco de Dados
Ao trabalhar com bancos de dados em aplicações Python, você frequentemente precisa abstrair operações de banco de dados. Aqui está como a injeção de dependência ajuda:
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]:
...
# Repositório depende da abstração
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
Esse padrão permite trocar implementações de banco de dados (PostgreSQL, SQLite, MongoDB) sem alterar seu código de repositório.
O Padrão Raiz de Composição
O Padrão Raiz de Composição é onde você monta todas as suas dependências no ponto de entrada da aplicação (normalmente main.py ou sua fábrica de aplicação). Isso centraliza a configuração de dependências e torna o gráfico de dependências explícito.
def create_app() -> FastAPI:
app = FastAPI()
# Inicializando dependências de infraestrutura
db = init_database()
logger = init_logger()
# Inicializando repositórios
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Inicializando serviços com dependências
email_svc = EmailService(logger)
payment_svc = PaymentService(logger)
user_svc = UserService(user_repo, logger)
order_svc = OrderService(order_repo, email_svc, logger, payment_svc)
# Inicializando manipuladores HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Conectando rotas
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Essa abordagem torna claro como sua aplicação está estruturada e de onde vêm as dependências. É particularmente valiosa ao construir aplicações seguindo princípios de arquitetura limpa, onde você precisa coordenar múltiplas camadas de dependências.
Frameworks de Injeção de Dependência
Para aplicações maiores com grafos de dependência complexos, gerenciar dependências manualmente pode se tornar trabalhoso. Python tem vários frameworks de DI que podem ajudar:
Dependency Injector
Dependency Injector é um framework popular que fornece uma abordagem baseada em contêiner para injeção de dependência.
Instalação:
pip install dependency-injector
Exemplo:
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
class Container(containers.DeclarativeContainer):
# Configuração
config = providers.Configuration()
# Banco de dados
db = providers.Singleton(
Database,
connection_string=config.database.url
)
# Repositórios
user_repository = providers.Factory(
UserRepository,
db=db
)
# Serviços
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 é uma biblioteca leve inspirada no Guice da Google, com foco em simplicidade.
Instalação:
pip install injector
Exemplo:
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 usar frameworks
Use um framework quando:
- Seu gráfico de dependência é complexo com muitos componentes interdependentes
- Você tem várias implementações da mesma interface que precisam ser selecionadas com base na configuração
- Você deseja resolução automática de dependências
- Você está construindo uma aplicação grande onde a fiação manual se torna propensa a erros
Mantenha a DI manual quando:
- Sua aplicação é pequena a média
- O gráfico de dependência é simples e fácil de seguir
- Você deseja manter as dependências mínimas e explícitas
- Você prefere código explícito em vez de magia de framework
Testando com Injeção de Dependência
Um dos principais benefícios da injeção de dependência é a melhoria na testabilidade. Aqui está como a DI torna o teste mais fácil:
Exemplo de Teste Unitário
from unittest.mock import Mock
import pytest
# Implementação mock para testes
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
# Teste usando o 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"
Esse teste executa rapidamente, não requer um banco de dados e testa sua lógica de negócios em isolamento. Ao trabalhar com testes unitários em Python, a injeção de dependência torna fácil criar duplos de teste e verificar interações.
Usando Fixtures do pytest
Fixtures do pytest funcionam excelente com injeção de dependência:
@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"
Padrões Comuns e Boas Práticas
1. Use a Segregação de Interfaces
Mantenha protocolos e interfaces pequenos e focados no que o cliente realmente precisa:
# Bom: O cliente só precisa ler usuários
class UserReader(Protocol):
def find_by_id(self, user_id: int) -> Optional['User']:
...
def find_by_email(self, email: str) -> Optional['User']:
...
# Interface separada para escrita
class UserWriter(Protocol):
def save(self, user: 'User') -> 'User':
...
def delete(self, user_id: int) -> bool:
...
2. Valide Dependências nos Construtores
Construtores devem validar dependências e levantar erros claros se a inicialização falhar:
class UserService:
def __init__(self, repo: UserRepository):
if repo is None:
raise ValueError("user repository cannot be None")
self.repo = repo
3. Use Dicas de Tipo
Dicas de tipo tornam dependências explícitas e ajudam com suporte do IDE e verificação de tipo estática:
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. Evite a Injeção Excessiva
Não injete dependências que são realmente detalhes de implementação interna. Se um componente cria e gerencia seus próprios objetos auxiliares, está tudo bem:
# Bom: O helper interno não precisa de injeção
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
# Cache interno - não precisa de injeção
self._cache: dict[int, User] = {}
5. Documente Dependências
Use docstrings para documentar por que as dependências são necessárias e quaisquer restrições:
class UserService:
"""UserService lida com a lógica de negócios relacionada a usuários.
Args:
repo: UserRepository para acesso a dados. Deve ser thread-safe
se usado em contextos concorrentes.
logger: Logger para rastreamento de erros e depuração.
"""
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
Injeção de Dependência com FastAPI
FastAPI tem suporte integrado para injeção de dependência por meio de seu 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
O sistema de injeção de dependência de FastAPI lida automaticamente com o gráfico de dependência, tornando fácil construir APIs limpas e mantíveis.
Quando NÃO usar Injeção de Dependência
A injeção de dependência é uma ferramenta poderosa, mas nem sempre é necessária:
Pule a DI para:
- Objetos de valor simples ou classes de dados
- Funções ou utilitários internos
- Scripts de uso único ou utilitários pequenos
- Quando a instânciação direta for mais clara e simples
Exemplo de quando NÃO usar DI:
# Classe de dados simples - não é necessário usar DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Utilitário simples - não é necessário usar DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integração com o Ecossistema Python
A injeção de dependência funciona de forma perfeita com outros padrões e ferramentas Python. Ao construir aplicações que usam pacotes Python ou frameworks de testes unitários, você pode injetar esses serviços em sua lógica de negócios:
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"Gerado relatório {report_id}")
return pdf
Isso permite trocar implementações ou usar mocks durante os testes.
Injeção de Dependência Assíncrona
A sintaxe async/await do Python funciona bem com injeção de dependência:
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]
Conclusão
A injeção de dependência é um pilar da escrita de código Python mantível e testável. Ao seguir os padrões apresentados neste artigo — injeção por construtor, design baseado em protocolos e o padrão raiz de composição — você criará aplicações mais fáceis de entender, testar e modificar.
Comece com a injeção manual por construtor para aplicações pequenas a médias, e considere frameworks como dependency-injector ou injector conforme seu gráfico de dependência crescer. Lembre-se de que o objetivo é clareza e testabilidade, não complexidade por si só.
Para mais recursos de desenvolvimento em Python, consulte nosso Folha de Dicas de Python para referência rápida sobre sintaxe Python e padrões comuns.
Links Úteis
- Folha de Dicas de Python
- Testes Unitários em Python
- Padrões de Design em Python para Arquitetura Limpa
- Saída Estruturada - LLMs no Ollama com Qwen3 - Python e Go
- Comparação de Saída Estruturada entre provedores populares de LLM - OpenAI, Gemini, Anthropic, Mistral e AWS Bedrock
- Construindo uma AWS Lambda Dual-Mode com Python e Terraform
Recursos Externos
- Injeção de Dependência em Python - Real Python
- Como a Injeção de Dependência em Python Melhora a Estrutura do Código - Volito Digital
- Tutorial de Injeção de Dependência em Python - DataCamp
- Dependency Injector - Documentação Oficial
- Injector - Framework de DI Leve
- Dependências no FastAPI - Documentação Oficial
- Princípios SOLID em Python - Lexicon de Padrões de Software
- Protocolos e Subtipagem Estrutural em Python - PEP 544