O que realmente acontece quando você importa um módulo
Muita gente acha que propriedade de modulo é só importar algo com import e pronto. A verdade é que o sistema não funciona assim tão simplesmente. Quando o Python carrega um módulo pela primeira vez, ele executa todo o código na ordem em que está escrito, cria os nomes no namespace e armazena o resultado no sys.modules. Na segunda vez que alguém chamar import, nada disso se repete. O módulo já está lá, cacheado. Isso parece óbvio até você enfrentar um problema real. Eu estava debugando um serviço Django rodando em produção quando percebi que uma configuração carregada num módulo utilitário estava trazendo valores quebrados. O módulo tinha sido importado no startup do gunicorn, mas o ambiente de variáveis de ambiente estava sendo sobrescrito depois por um script de inicialização. Como o Python já tinha cacheado o módulo, qualquer import subsequente usava os valores errados de configuração. A solução foi limpar explicitamente o entry do sys.modules antes de fazer o import novamente, junto com uma função de reload forçado.
propriedade de modulo e como o cache afeta imports
O comportamento de cache é a parte mais importante que as pessoas ignoram. sys.modules é basicamente um dicionário global que mapeia nomes de módulo para objetos de módulo já criados. Quando você faz import foo, o interpretador primeiro verifica esse dicionário. Se foo estiver lá, ele retorna o objeto imediatamente, sem executar o arquivo .py de novo. Se não estiver, ele procura o arquivo, executa, e guarda o resultado no dicionário. Tem uma armadilha comum aqui. Se você deletar manualmente um entry do sys.modules e tentar importar de novo, o módulo vai ser recarregado sim, mas as variáveis que já foram atribuídas em outros módulos que importaram foo anteriormente não vão se atualizar. Elas continuam apontando para o objeto antigo que estava em memória. Isso causa bugs difíceis de rastrear porque nada dá erro. Apenas comportamento estranho que só aparece em cenários específicos de reload.
Para recarregar um módulo de verdade, existe o importlib.reload(). Ele reinicia a execução do módulo usando o mesmo namespace, substituindo funções, classes e variáveis. Mas mesmo o reload tem limitações. Classes que já foram instanciadas continuam com a versão antiga. Funções que receberam referências a objetos do módulo antes do reload também continuam com as versões antigas. É útil para desenvolvimento interativo, mas não confiável para produção. Outro ponto que poucas pessoas mencionam: módulos embutidos no interpretador, como sys e builtins, nunca saem do sys.modules. Você não consegue recarregá-los de jeito nenhum. Tentar dar reload num módulo built-in gera TypeError. Isso é relevante quando você escreve testes que precisam injetar dependências — não adianta tentar burlar o cache desses módulos, o behavior é fixo.
Em projetos grandes, o custo de imports repetidos costuma ser mal estimado. A primeira importação de um módulo pesado como pandas ou numpy pode levar de 2 a 5 segundos. Depois disso, o custo é praticamente zero porque o cache entra em ação. Mas se você está rodando testes unitários e cada teste faz um import de tudo de novo sem compartilhar o ambiente, o tempo sobe rapidamente. A recomendação prática é usar fixtures compartilhadas no pytest para que o módulo seja carregado uma única vez por sessão. Quando o assunto é namespace, propriedade de modulo determina exatamente o que fica acessível de fora. Se um arquivo módulo define variáveis começando com underscore, elas não entram no que é exposto por um from modulo import *, mas continuam disponíveis diretamente pelo nome completo. Isso é intencional e útil para manter API pública limitada sem criar arquivos separados. Porém, existem casos em que você precisa controlar explicitamente o que sai. A lista __all__ no topo do arquivo resolve isso. Se você definir __all__ = ['funcao_a', 'classe_b'], o import * só traz esses nomes, independente dos outros que existirem no módulo.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Há também a questão dos submódulos. Quando você faz import package.submodule, o package principal é carregado primeiro, e aí o submódulo. O pacote pai fica no sys.modules e o submódulo também. Se o código do pacote pai importar o submódulo internamente durante a inicialização, o submódulo já estará disponível quando você tentar importar externamente. Isso pode criar dependências ocultas onde um módulo funciona porque outro foi importado antes por acaso, mas quebra em outro contexto. O problema inverso também existe. Se o submódulo depende do pacote pai ter sido configurado, e você importar só o submódulo sem passar pelo pacote, algumas coisas podem falhar silenciosamente. Variáveis que deveriam existir no namespace do pacote simplesmente não estarão lá. O erro pode ser AttributeError ou mesmo NoneType sendo usado como se fosse um objeto válido.
Para evitar esses problemas, use importações relativas dentro do seu pacote e evite depender da ordem de importação do código chamador. Coloque configurações obrigatórias em funções explícitas de init que precisam ser chamadas antes de qualquer coisa. Isso deixa claro o que é responsabilidade do módulo e o que é responsabilidade de quem está usando. Performance também entra na conta em alguns cenários. Cada módulo ocupa memória proporcional ao tamanho do seu código e dos objetos que ele cria. Módulos com muitas classes e funções pesadas, especialmente os que instanciam objetos grandes no nível do módulo, podem consumir bastante memória só pelo ato de serem carregados. Em serviços com many workers, cada worker carrega sua própria cópia do módulo. Se você tem 8 workers e cada módulo pesado consome 50MB, são 400MB só de módulos carregados independentemente.
Uma alternativa interessante para casos onde você precisa de lógica compartilhada mas não quer o overhead de importação é usar functools.lru_cache em funções que carregam dados pesados. Em vez de carregar um módulo inteiro só por uma função específica, você carrega sob demanda e cacheia o resultado. Isso reduz o footprint inicial e evita imports desnecessários em rotas que nunca usam aquela funcionalidade. A propriedade de módulo também se aplica a módulos escritos em C. Eles seguem as mesmas regras de cache no sys.modules, mas a execução do código não acontece da mesma forma. Módulos C são compilados e carregados como bibliotecas dinâmicas. O ponto de entrada é uma função de inicialização específica. Se você tentar dar reload num módulo C, o comportamento é imprevisível porque a biblioteca já está carregada pelo processo. Na prática, módulos C nunca devem ser alvo de reload em cenários reais.
Documentação técnica sobre o tema costuma ser incompleta. O tutorial oficial do Python explica importação superficialmente, mas não entra nos detalhes de como o cache funciona, nem nos edge cases que aparecem em produção. O documento mais completo é a referência de linguagem, seção 6.3, mas ela é densa e técnica demais para quem está começando. O equilíbrio ideal é ler a referência depois de ter visto pelo menos um bug causado por comportamento de cache inesperado. Na prática, saber como propriedade de modulo funciona no dia a dia evita metade dos bugs de importação que aparecem em projetos Python. O resto depende de organização do código e de testar em ambientes que reflitam a produção, não apenas no seu computador local onde a ordem de execução pode ser diferente.