Injeção de Dependência: A Maneira Python
Padrões de Injeção de Dependência 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 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 os 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 a protocolos, classes abstratas base e tipagem por pato. A flexibilidade do Python significa que você pode implementar padrões 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 escrever 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 substituir dependências reais por duplos de teste.
Melhor Mantibilidade: As dependências tornam-se explícitas no seu código. Quando você olha para um construtor, você vê imediatamente o que um componente requer. Isso torna a base de código mais fácil de entender e modificar.
Acoplamento Fraco: 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 implementações diferentes para ambientes diferentes (desenvolvimento, teste, produção) sem alterar a lógica de negócios. Isso é especialmente útil ao implantar aplicações Python em diferentes plataformas, seja em AWS Lambda ou servidores tradicionais.
Injeção de Construtor: A Maneira Python
A maneira mais comum e idiomática de implementar injeção de dependência em Python é através da injeção de construtor — aceitando dependências como parâmetros no método __init__.
Exemplo Básico
Aqui está um exemplo simples demonstrando injeção de construtor:
from typing import Protocol
from abc import ABC, abstractmethod
# Defina 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)
Este padrão torna claro que UserService requer um UserRepository. Você não pode criar um UserService sem fornecer um repositório, o que previne erros de tempo de execução devido a dependências faltantes.
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 Abstratas Base
Um dos princípios-chave 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 Abstratas Base (ABCs) (tipagem nominal).
Protocolos (Python 3.8+)
Protocolos usam tipagem estrutural — se um objeto tem 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 este 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 Abstratas Base
ABCs usam tipagem nominal — as classes devem herdar explicitamente do 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 do Mundo Real: Abstração de Banco de Dados
Ao trabalhar com bancos de dados em aplicações Python, você frequentemente precisará abstrair operações de banco de dados. Veja 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
Este padrão permite que você alterne implementações de banco de dados (PostgreSQL, SQLite, MongoDB) sem alterar seu código de repositório.
O Padrão Composition Root (Raiz de Composição)
A Composition Root é onde você monta todas as suas dependências no ponto de entrada da aplicação (tipicamente main.py ou sua fábrica de aplicação). Isso centraliza a configuração de dependências e torna o grafo de dependências explícito.
def create_app() -> FastAPI:
app = FastAPI()
# Inicialize dependências de infraestrutura
db = init_database()
logger = init_logger()
# Inicialize repositórios
user_repo = UserRepository(db)
order_repo = OrderRepository(db)
# Inicialize 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)
# Inicialize manipuladores HTTP
user_handler = UserHandler(user_svc)
order_handler = OrderHandler(order_svc)
# Conecte rotas
app.include_router(user_handler.router)
app.include_router(order_handler.router)
return app
Essa abordagem torna claro como sua aplicação é estruturada e de onde vêm as dependências. É particularmente valioso 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 DI que podem ajudar:
Dependency Injector
Dependency Injector é um framework popular que fornece uma abordagem baseada em container 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 do Google, focando 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 grafo de dependência for complexo com muitos componentes interdependentes
- Você tiver múltiplas implementações da mesma interface que precisam ser selecionadas com base na configuração
- Você quiser resolução de dependência automática
- Você estiver construindo uma aplicação grande onde o cabeamento manual se torna propenso a erros
Mantenha a DI manual quando:
- Sua aplicação for de pequeno a médio porte
- O grafo de dependência for simples e fácil de seguir
- Você quiser manter as dependências mínimas e explícitas
- Você preferir código explícito a “mágica” de frameworks
Testando com Injeção de Dependência
Um dos principais benefícios da injeção de dependência é a testabilidade melhorada. Veja como a DI torna os testes mais fáceis:
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"
Este teste roda rapidamente, não requer um banco de dados e testa sua lógica de negócios de forma isolada. 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 Melhores Práticas
1. Use Segregação de Interface
Mantenha protocolos e interfaces pequenos e focados no que o cliente realmente precisa:
# Bom: 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("repositório de usuário não pode ser None")
self.repo = repo
3. Use Type Hints (Dicas de Tipo)
Type hints tornam as dependências explícitas e ajudam com suporte de IDE e verificação 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. Evite Sobre-Injeção
Não injete dependências que são verdadeiros detalhes internos de implementação. Se um componente cria e gerencia seus próprios objetos auxiliares, isso é aceitável:
# Bom: Auxiliar 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 dependências são necessárias e quaisquer restrições:
class UserService:
"""UserService lida com 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
O FastAPI tem suporte nativo para injeção de dependência através do 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 do FastAPI gerencia o grafo de dependências automaticamente, facilitando a construção de 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 data classes
- Funções auxiliares internas ou utilitários
- Scripts únicos ou utilitários pequenos
- Quando a instância direta for mais clara e simples
Exemplo de quando NÃO usar DI:
# Dataclass simples - sem necessidade de DI
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# Utilitário simples - sem necessidade de DI
def format_currency(amount: float) -> str:
return f"${amount:.2f}"
Integração com o Ecossistema Python
A injeção de dependência funciona perfeitamente com outros padrões e ferramentas do Python. Ao construir aplicações que usam pacotes Python ou frameworks de teste unitário, 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"Generated report {report_id}")
return pdf
Isso permite que você alterne implementações ou use 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 é uma pedra angular na escrita de código Python mantível e testável. Seguindo os padrões descritos neste artigo — injeção de construtor, design baseado em protocolos e o padrão Composition Root — você criará aplicações mais fáceis de entender, testar e modificar.
Comece com injeção de construtor manual para aplicações de pequeno a médio porte, e considere frameworks como dependency-injector ou injector conforme seu grafo de dependências cresce. Lembre-se que o objetivo é clareza e testabilidade, não complexidade por si só.
Para mais recursos de desenvolvimento Python, consulte nossa Lista de Atalhos Python para referência rápida de sintaxe Python e padrões comuns.
Links Úteis
- Lista de Atalhos Python
- Testes Unitários em Python
- Padrões de Design 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 um AWS Lambda de Modo Dual 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 Python - DataCamp
- Dependency Injector - Documentação Oficial
- Injector - Framework DI Leve
- Dependências FastAPI - Docs Oficiais
- Princípios SOLID em Python - Software Patterns Lexicon
- Protocolos Python e Subtipagem Estrutural - PEP 544