Skip to content
Tópicos
Python
Python Circular Import: How to Fix It (With Working Examples)

Importação Circular em Python: Como Corrigir (Com Exemplos)

Publicado em

Atualizado em

Uma importação circular (circular import) acontece quando o módulo A importa o módulo B e B (direta ou indiretamente) também importa A. O Python não fica preso em um loop infinito na maioria dos casos. O que você vê é uma importação parcial: um módulo ainda está carregando quando o outro tenta ler um nome dele — e aí surge ImportError / AttributeError, ou objetos só pela metade.

Correção rápida (comece aqui)

SituaçãoO que fazer primeiro
Dois módulos se importam no topo do arquivoMova o código compartilhado para um terceiro módulo que os dois possam importar
Você só precisa de um nome dentro de uma funçãoImporte dentro dessa função (lazy import)
Você só precisa de tipos para anotaçõesUse from typing import TYPE_CHECKING e aspas nas anotações se precisar
Pacote grande com ciclos profundosRedesene as dependências para que camadas baixas nunca importem camadas altas

Padrão mínimo que costuma destravar na hora:

# Instead of a top-level cycle:
# models.py imports services.py
# services.py imports models.py
 
# services.py — import only where used
def process_user(user_id: int):
    from models import User  # lazy import breaks the load-time cycle
    return User.get(user_id)

Se isso parecer gambiarra, continue lendo. A correção duradoura quase sempre é direção de dependência, não um truque esperto de import.

Como uma importação circular se manifesta

Exemplo mínimo que falha

Crie dois módulos irmãos:

# a.py
import b
 
def hello_from_a():
    return "A"
 
print("a loaded", b.hello_from_b())
# b.py
import a
 
def hello_from_b():
    return "B"
 
print("b loaded", a.hello_from_a())

Execute:

python a.py

Falha típica (o texto varia um pouco entre versões do Python):

ImportError: cannot import name 'hello_from_a' from partially initialized module 'a'
(most likely due to a circular import)

Essa mensagem é a forma de busca desse problema. O Python começou a carregar a, que começou a carregar b, que tentou terminar a importação de a antes de a terminar de executar.

O que realmente dá errado

Quando o Python importa um módulo:

  1. Cria um objeto de módulo vazio e coloca em sys.modules.
  2. Executa o corpo do módulo de cima para baixo.
  3. Só depois que esse corpo termina é que todos os nomes de nível superior têm garantia de existir.

Durante um ciclo, o passo 2 do módulo A ainda está rodando quando B pede algo definido mais abaixo em A. O nome não existe → erro de importação. É uma falha de inicialização parcial (partially initialized module), não um loop sem fim.

Fluxo de diagnóstico

Siga esta ordem. Pare quando o ciclo sumir.

  1. Reproduza com o mesmo ponto de entrada
    Rode o mesmo arquivo que o erro menciona (python -m package.module ou o caminho do script). Importação circular é sensível ao entrypoint.

  2. Leia o traceback de baixo para cima
    Os últimos frames costumam mostrar module_x importando module_y enquanto module_y já está na pilha.

  3. Desenhe um esboço de dependência em uma direção
    Liste cada módulo e o que ele importa. Qualquer seta que “suba” de modelos/core para app/UI/API é suspeita.

  4. Classifique a necessidade

    • Valor/função em runtime → reestruture ou lazy import
    • Só tipo → TYPE_CHECKING
    • Constantes/modelos compartilhados → extraia para um módulo folha
  5. Aplique a menor correção correta
    Prefira extrair módulo compartilhado > lazy import > redesenhar o layout do pacote. Evite o mito de que “import absoluto resolve”.

  6. Rode de novo pelo mesmo entrypoint
    Confirme tanto o estilo python a.py quanto o -m de pacote, se você usa os dois.

Correção 1: Extrair um terceiro módulo (melhor padrão)

Quando models e services precisam de User, não faça um importar o outro. Coloque User em um módulo folha que não depende das camadas de cima.

app/
  models/user.py      # leaf: no import from services
  services/billing.py # imports models.user
  api/routes.py       # imports services
# models/user.py
class User:
    def __init__(self, user_id: int, email: str):
        self.user_id = user_id
        self.email = email
 
    @classmethod
    def get(cls, user_id: int) -> "User":
        return cls(user_id, f"user{user_id}@example.com")
# services/billing.py
from models.user import User
 
def invoice(user_id: int) -> str:
    user = User.get(user_id)
    return f"Invoice for {user.email}"
# api/routes.py
from services.billing import invoice
 
def handle(user_id: int) -> str:
    return invoice(user_id)

A direção da dependência é única: api → services → models. O ciclo some porque nada de baixo importa nada de cima.

Correção 2: Lazy import (import local)

Importe dentro da função ou método que precisa do símbolo quando um ciclo temporário é difícil de desatar.

# reporters.py
def build_report(order_id: str) -> dict:
    from orders import Order  # imported at call time, not module load time
 
    order = Order.load(order_id)
    return {"order_id": order.id, "total": order.total}
# orders.py
class Order:
    def __init__(self, id: str, total: float):
        self.id = id
        self.total = total
 
    @classmethod
    def load(cls, order_id: str) -> "Order":
        return cls(order_id, 19.99)
 
    def pretty(self) -> str:
        from reporters import build_report  # only if you truly need this edge
        return str(build_report(self.id))

Quando lazy import faz sentido

  • Quebrar um ciclo teimoso rápido em código legado
  • Dependências opcionais e pesadas (importar só no caminho que precisa)

Quando evitar como desenho de longo prazo

  • Caminhos quentes em que o custo de import importa (em geral pequeno, mas mensurável)
  • Arquitetura que continua gerando ciclos novos; nesse caso, extraia módulos

Correção 3: TYPE_CHECKING para imports só de anotação

Se o único motivo do import é type hint, deixe-o fora do runtime:

from __future__ import annotations
from typing import TYPE_CHECKING
 
if TYPE_CHECKING:
    from models.user import User  # not imported at runtime
 
def notify(user: User, message: str) -> None:
    print(user.email, message)

Com from __future__ import annotations (ou anotações entre aspas "User"), o Python não precisa de User em runtime para a anotação funcionar. Essa é a ferramenta certa para ciclos de type hints, não para chamar métodos em User.

Correção 4: Injetar dependências em vez de importá-las

Para serviços que “precisam um do outro”, passe colaboradores por parâmetro em vez de importar globalmente.

# notifications.py
class Notifier:
    def send(self, email: str, body: str) -> None:
        print(f"to={email} body={body}")
 
 
# checkout.py
class Checkout:
    def __init__(self, notifier: "Notifier"):
        self.notifier = notifier
 
    def complete(self, email: str) -> None:
        # business logic...
        self.notifier.send(email, "Order complete")
# main.py
from notifications import Notifier
from checkout import Checkout
 
checkout = Checkout(Notifier())
checkout.complete("a@example.com")

Nem notifications nem checkout importam o outro. A composição na borda (main.py) cuida da fiação. É a mesma ideia de injeção por construtor em apps e frameworks maiores.

Armadilhas comuns (conselhos que não quebram o ciclo)

Esses aparecem em guias antigos. Resolvem outros problemas, não importação circular.

Conselho que você pode verRealidade
“Use import absoluto”Import absoluto melhora clareza. Não remove dependência mútua.
“Defina __all__Só controla a superfície de from module import *. Zero efeito no ciclo.
“Sempre use importlib.import_moduleImport dinâmico pode adiar o carregamento, mas se você ainda importar na hora errada, o ciclo permanece. Prefira import local explícito ou reestruturação.
“O Python entra em loop infinito”O resultado usual é ImportError / inicialização parcial, não um loop rodando para sempre.
“Renomeie o arquivo e torça”Conflito de nome pode gerar erro confuso de import, mas renomear sozinho não corrige uma dependência real A↔B.

Padrões de layout de pacote que evitam ciclos

Pacotes saudáveis parecem um DAG (grafo acíclico dirigido):

package/
  __init__.py          # keep thin; avoid importing everything eagerly
  domain/              # pure models, no IO
  services/            # use domain
  adapters/            # DB, HTTP, filesystem
  api/                 # entrypoints; imports services only

Regras práticas:

  1. Módulos folha não importam nada do app.
  2. __init__.py deve ficar enxuto. Cadeias ansiosas de from .a import * / from .b import * são fábrica clássica de ciclo.
  3. Prefira imports explícitos de módulos folha estáveis em vez de reexportações “de conveniência” que puxam metade do pacote.
  4. Rode como pacote ao desenvolver bibliotecas: python -m package.api evita gambiarras de path que mascaram dependências reais. Veja também como executar scripts Python.

Se você mexe muito em arquivos ao refatorar, pathlib deixa scripts de mover/renomear mais claros do que cola de path em string.

Kit de depuração

Rastrear onde o ciclo entra

# debug_import.py
import sys
import trace
 
tracer = trace.Trace(count=False, trace=True)
tracer.runfunc(lambda: __import__("your_package.entry"))

No dia a dia, o traceback basta. Em codebases grandes, ferramentas ajudam:

  • python -X importtime -c "import your_package" — mostra ordem e custo de import
  • import-linter (terceiros) — codifica “services não podem importar api” como regra de CI
  • pyright / mypy — pegam erros de TYPE_CHECKING cedo quando você usa anotações

Capturar sintomas de inicialização parcial em testes

def test_package_imports_cleanly():
    import importlib
    import your_package.api as api
 
    importlib.reload(api)  # optional stress
    assert hasattr(api, "handle")

Se os testes unitários só importam módulos minúsculos isolados, eles podem não ver ciclos que só aparecem pelo entrypoint real do app. Adicione um smoke test que importa o módulo de entrada de verdade.

Tabela de decisão: qual correção usar?

SinalPrefira
Modelo/constante compartilhado pelos dois ladosExtrair terceiro módulo
Só um caminho de chamada precisa do outro móduloLazy import nessa função
Import existe só para o type checkerTYPE_CHECKING
Dois serviços se orquestram mutuamenteInjeção de dependência / callbacks / eventos
Ciclos voltam depois de remendosRegras de camadas do pacote + lint no CI

FAQ

Conclusão

Importação circular é um problema de grafo de dependências. O Python mostra isso como erro de inicialização parcial — em especial a mensagem “most likely due to a circular import”. Corrija tornando as dependências unidirecionais: extraia folhas compartilhadas, use lazy import só onde for necessário, mantenha imports de anotação atrás de TYPE_CHECKING e ligue colaboradores de fora quando dois serviços se precisam.

Ignore “folclore” de correção — import absoluto, __all__ e renomear arquivo não dissolvem um ciclo real. Com o grafo limpo, o import volta a ser chato — e é exatamente isso que você quer.

Guias relacionados