Container de Injecao de Dependencia em Python Puro: Seu Codigo Modular Sem Framework (DI Que Funciona)
Por Que Seus Scripts Python Viram Espaguete Depois do Terceiro Modulo
Todo mundo comeca com a melhor das intencoes. Um script limpo, uma funcao que faz uma coisa, imports organizados. A
vem a segunda automacao. Depois a terceira. De repente, seu main.py tem 800 linhas, importa 14 modulos que importam entre si em circulo, e voce tem medo de mexer em qualquer coisa porque sempre quebra algo que parecia nao ter relacao.
Se isso soa familiar, respira. Voce nao escreveu codigo ruim — voce escreveu codigo que cresceu sem arquitetura. E existe um padrao que resolve exatamente isso: o container de injecao de dependencia (DI Container). Sim, aquele conceito que parece coisa de framework enterprise pesado, mas que em Python puro cabe em menos de 100 linhas.
Neste artigo do Lab da Garra, vou te mostrar como construir um container DI funcional em Python puro, sem bibliotecas externas, e como ele pode transformar suas automacoes de um amontoado de imports acoplados em um sistema modular e testavel. Spoiler: eu levei tres tentativas erradas ate acertar, e vou mostrar cada uma delas.
O Que e Injecao de Dependencia (e Por Que Voce Ja Precisou Disso)
Vamos direto ao ponto. Injecao de dependencia e, na essencia, um padrao onde os objetos nao criam suas proprias dependencias. Em vez disso, alguem de fora entrega essas dependencias prontas.
Sem DI, e assim:
class NotificacaoService:
def __init__(self):
self.db = Database()
self.email = EmailClient()
self.sms = SmsGateway()
def enviar_alerta(self, mensagem):
self.db.log(mensagem)
self.email.send(mensagem)
O problema? Se quiser testar NotificacaoService sem enviar email de verdade, voce precisa mockar o __init__, o que e gambiarra pura. Se quiser trocar Database por SQLite, precisa reescrever a classe.
Com injecao de dependencia:
class NotificacaoService:
def __init__(self, db, email_client, sms_gateway=None):
self.db = db
self.email = email_client
self.sms = sms_gateway
def enviar_alerta(self, mensagem):
self.db.log(mensagem)
self.email.send(mensagem)
Agora o teste passa objetos fake. A producao passa objetos reais. A classe nao sabe de onde vieram. Isso e inversao de controle na pratica.
O Salto: De Injecao Manual Para um Container
“Beleza, injeto as dependencias no construtor.” Funciona para dois ou tres servicos. Mas quando voce tem 12 modulos de automacao, cada um dependendo de outros tres, a montagem manual vira um pesadelo:
# main.py - o arquivo que ninguem quer manter
db = Database()
cache = RedisCache(db)
email = EmailClient(db, cache)
sms = SmsGateway(db, cache)
scheduler = TaskScheduler(db, email, sms, cache)
logger = Logger(db, email)
monitor = MonitorService(scheduler, logger, db)
notificacao = NotificacaoService(db, email, sms)
dashboard = DashboardService(monitor, notificacao, logger)
Isso e o que chamamos de composition root — o ponto onde tudo se conecta. E quando esse ponto cresce sem controle, qualquer mudanca exige rastrear quem depende de quem.
Um container de DI resolve isso: voce registra as dependencias uma vez, e o container resolve a arvore inteira automaticamente.
Construindo um Container DI em Python Puro
Vamos construir algo simples mas funcional. Tres conceitos: registro (dizer ao container como criar cada servico), resolucao (instanciar com todas as dependencias resolvidas) e escopo (singleton vs transient).
from typing import Any, Callable, Dict
class Container:
def __init__(self):
self._singletons: Dict[str, Any] = {}
self._factories: Dict[str, Callable] = {}
self._resolved: Dict[str, Any] = {}
def singleton(self, name: str, factory: Callable):
self._factories[name] = factory
def resolve(self, name: str) -> Any:
if name in self._resolved:
return self._resolved[name]
if name not in self._factories:
raise ValueError(f"Servico '{name}' nao registrado")
factory = self._factories[name]
instance = factory(self)
if name in self._singletons:
self._resolved[name] = instance
return instance
def __getitem__(self, name: str) -> Any:
return self.resolve(name)

“Espera, cade a magica?” A magica esta no uso. Veja como registrar e resolver:
container = Container()
container.singleton("db", lambda ctx: Database(config=DB_CONFIG))
container.singleton("cache", lambda ctx: RedisCache(ctx.resolve("db")))
container.singleton("email", lambda ctx: EmailClient(ctx.resolve("db")))
container.singleton("sms", lambda ctx: SmsGateway(ctx.resolve("db")))
container.singleton("notificacao", lambda ctx: NotificacaoService(
db=ctx.resolve("db"),
email_client=ctx.resolve("email"),
sms_gateway=ctx.resolve("sms")
))
notificacao = container.resolve("notificacao")
notificacao.enviar_alerta("Container funcionando!")
O container cuida da ordem de resolucao. Se notificacao precisa de email e email precisa de db, o container resolve db primeiro, depois email, depois notificacao. Sem voce precisar pensar na sequencia.
Versao 2: Auto-Resolucao com Type Hints
A versao acima funciona, mas ainda exige que voce escreva cada factory manualmente. Da pra melhorar usando type hints e introspeccao:
from typing import get_type_hints
class AutoContainer(Container):
def register(self, cls, *, singleton=True):
if singleton:
self._singletons[cls.__name__] = True
def factory(ctx):
hints = get_type_hints(cls.__init__)
kwargs = {}
for pname, ptype in hints.items():
if pname in ("return", "self"):
continue
kwargs[pname] = ctx.resolve(ptype.__name__)
return cls(**kwargs)
self._factories[cls.__name__] = factory
return cls
container2 = AutoContainer()
container2.register(Database, singleton=True)
container2.register(RedisCache, singleton=True)
container2.register(EmailClient, singleton=True)
container2.register(SmsGateway, singleton=True)
container2.register(NotificacaoService, singleton=True)
notificacao = container2.resolve("NotificacaoService")
Isso funciona quando as dependencias sao tipadas no __init__:
class NotificacaoService:
def __init__(self, db: Database, email_client: EmailClient, sms_gateway: SmsGateway):
self.db = db
self.email = email_client
self.sms = sms_gateway

O Perrengue Que Me Fez Reescrever o Container Duas Vezes
A segunda versao caiu no problema oposto: ciclos de dependencia. O MonitorService precisava do Logger, e o Logger precisava do MonitorService para registrar metricas de performance. O container entrava em loop infinito. A solucao? Um detector de ciclos que lanca erro antes de travar:
class ContainerComCiclo(Container):
def __init__(self):
super().__init__()
self._resolving = set()
def resolve(self, name):
if name in self._resolving:
raise RecursionError(
f"Ciclo detectado: {name} -> {' -> '.join(self._resolving)}"
)
self._resolving.add(name)
try:
return super().resolve(name)
finally:
self._resolving.discard(name)
Com isso, em vez de um crash misterioso, voce recebe: Ciclo detectado: MonitorService -> Logger -> MonitorService. Exatamente onde o problema esta.
Integrando com Systemd e Automacoes Reais
Container sem uso pratico e exercicio academico. Veja como integrar com um servico systemd que gerencia multiplas automacoes:
#!/usr/bin/env python3
# /opt/automente/services/container_config.py
app_container = ContainerComCiclo()
app_container.singleton("config", lambda ctx: load_config("/etc/automente/config.yaml"))
app_container.singleton("db_log", lambda ctx: sqlite3.connect("/var/lib/automente/logs.db"))
app_container.singleton("db_dados", lambda ctx: PostgresPool(ctx["config"].db_url))
app_container.singleton("logger", lambda ctx: Logger(ctx["db_log"]))
app_container.singleton("email", lambda ctx: SMTPClient(ctx["config"].smtp))
app_container.singleton("notificacao", lambda ctx: NotificacaoService(
logger=ctx["logger"], email=ctx["email"]))
app_container.singleton("scheduler", lambda ctx: TaskScheduler(
db=ctx["db_dados"], notificacao=ctx["notificacao"]))
app_container.singleton("monitor", lambda ctx: MonitorService(
scheduler=ctx["scheduler"], logger=ctx["logger"]))
def main():
monitor = app_container.resolve("monitor")
monitor.start()
Quando um novo modulo entra no sistema, voce so registra no container. Nao mexe no main(). Nao quebra nada existente. E assim que sistemas crescem sem virar espaguete.
Testando Com o Container (Finalmente Facil)
Aqui esta o beneficio que ninguem conta: com DI, testar vira trivial. Em vez de mockar imports ou usar unittest.mock.patch em tudo, voce passa objetos fake diretamente:
class FakeDB:
def __init__(self):
self.logs = []
def log(self, msg):
self.logs.append(msg)
class FakeEmail:
def __init__(self):
self.sent = []
def send(self, msg):
self.sent.append(msg)
def test_notificacao_envia_email():
db = FakeDB()
email = FakeEmail()
svc = NotificacaoService(db, email)
svc.enviar_alerta("Teste!")
assert len(email.sent) == 1
assert "Teste!" in email.sent[0]
assert len(db.logs) == 1
Sem mocks, sem patches, sem magia. Objetos normais passando por construtor normal. Testes rodam em milissegundos porque nao ha rede, banco ou filesystem envolvido.
Quando NAO Usar Container de DI
Sou fa de DI, mas nao e bala de prata. Evite quando:
- Script unico com 50 linhas: overkill absoluto. Imports diretos resolvem.
- Performance critica em hot path: a resolucao do container tem overhead (pequeno, mas existe). Resolva uma vez no startup, nao a cada request.
- Dependencias que mudam em runtime: containers sao otimos para configuracao no startup. Se sua dependencia muda a cada 5 minutos baseado em dados externos, use uma factory dinamica, nao um singleton.
- Equipe que nao entende o padrao: DI mal implementado e pior que nenhum DI. Se o time nao sabe o que e inversao de controle, comece com injecao manual antes de introduzir o container.
Alternativas e Ecossistema Python
Se voce nao quer construir o seu, existem opcoes maduras:
- dependency-injector — o mais popular no Python, com providers, containers e wiring automatico. Suporte a YAML config.
- injector — inspirado no Guice do Java, usa decorators e type hints.
- fastapi Depends() — se usa FastAPI, ja tem DI embutido. Nao precisa de container separado.
Mas conhecer a versao “na unha” te da intuicao para debugar quando essas bibliotecas falham — e elas falham, especialmente com dependencias circulares e configs erradas. Eu ja gastei duas horas debugando um CircularDependencyError do dependency-injector antes de perceber que o problema era o meu entendimento, nao a biblioteca.
Links Uteis e Leitura Complementar
Se quer se aprofundar nos temas que cruzam com este artigo:
- Servidor de Configuracao Local com Python — combine com o container DI para ter configuracao centralizada
- Circuit Breaker com Python e SQLite — protecao de servicos que seu container gerencia
- Fila de Tarefas Assincronas — orquestracao que se beneficia de DI
- Categoria Lab da Garra — mais projetos praticos do laboratorio
Conclusao: Menos Espaguete, Mais Arquitetura
Container de injecao de dependencia nao e framework enterprise. E um padrao de organizacao que resolve um problema real: como conectar modulos sem criar emaranhado. Em Python, a versao funcional cabe em 50 linhas. A versao com auto-resolucao por type hints cabe em 80.
O beneficio real nao e o codigo que voce escreve — e o codigo que voce para de escrever: toda aquela logica de composicao manual, os imports condicionais, os singletons globais que tornam testes impossiveis.
Se voce tem automacoes que crescem alem de tres scripts, pare e construa o container antes que o espaguete se forme. E mais facil prevenir do que refatorar.
Agora me diz: qual automacao ou padrao de codigo voce quer ver desmontado no Lab da Garra? Manda nos comentarios ou sugere um tema — os melhores viram artigo na semana que vem.
