PEP 810 – imports explícitos com carregamento tardio
(pep-previews--4622.org.readthedocs.build)- Em Python, a convenção mais comum é declarar todos os imports no nível do módulo
- Porém, ao executar o programa, até módulos de dependências desnecessárias acabam sendo carregados imediatamente, causando problemas de velocidade de inicialização e uso de memória
- Até agora, o uso de imports manuais com atraso, como imports dentro de funções, era comum, mas tinha a desvantagem de dificultar a manutenção e o gerenciamento de dependências
- Este PEP 810 introduz uma sintaxe de import explícito com carregamento tardio com a nova palavra-chave
lazy, que é local, explícita, controlada e granular - Com esse recurso, os módulos são carregados somente quando realmente necessários, melhorando latência de inicialização e desperdício de memória ao mesmo tempo em que mantém a transparência da estrutura do código
Situação atual dos imports em Python e seus problemas
- Em Python, é amplamente adotada a prática de escrever os imports no topo do módulo
- Essa abordagem reduz duplicação, permite entender de relance a estrutura de dependências dos imports e minimiza overhead em tempo de execução ao importar apenas uma vez
- No entanto, quando o programa é executado e o primeiro módulo (
main) é carregado, é fácil ocorrer uma cadeia de imports em que até muitos módulos de dependência que não serão usados acabam sendo lidos imediatamente - Isso é especialmente problemático em ferramentas CLI, nas quais até chamar apenas a ajuda completa pode pré-carregar dezenas de módulos, gerando overhead desnecessário em todos os subcomandos
Alternativas existentes e seus problemas
- É comum usar abordagens manuais para adiar o momento do import, como mover imports para dentro de funções
- Porém, essa abordagem tem grandes desvantagens, como perda de consistência e manutenção, além de aumentar a dificuldade de entender todas as dependências
- Segundo a análise da biblioteca padrão, em códigos sensíveis a desempenho, cerca de 17% de todos os imports já são usados dentro de funções ou métodos com o objetivo de adiar imports
- Existem ferramentas relacionadas a import tardio, como
importlib.util.LazyLoadere o pacote third-partylazy_loader, mas elas não cobrem todos os casos ou carecem de um padrão único
PEP 810: introdução de imports explícitos com carregamento tardio
-
Introdução da nova palavra-chave suave
lazy(ela só tem significado em contextos específicos e também pode ser usada como nome de variável) -
lazysó pode ser usado antes da instrução import e não pode ser usado em escopos como função/classe/with/try nem em star import -
O comportamento é aplicado claramente por instrução de import, adiando o carregamento do módulo até o momento de uso
lazy import 모듈명 lazy from 모듈명 import 이름
Forma de implementação dos imports explícitos com carregamento tardio e regras sintáticas
-
Casos de erro de sintaxe:
- Não é permitido dentro de funções, classes,
try/withnem em star import (*)
- Não é permitido dentro de funções, classes,
-
Exemplo de uso:
import sys lazy import json print('json' in sys.modules) # False (ainda não carregado) result = json.dumps({"hello": "world"}) # carrega no primeiro uso print('json' in sys.modules) # True (carregamento do módulo tardio concluído) -
Também é possível declarar os alvos lazy no nível do módulo por meio do atributo
__lazy_modules__, como uma lista de strings__lazy_modules__ = ["json"] import json # tratado como lazy
Controle de comportamento com flag global e filtro
-
É possível controlar se o lazy será aplicado por módulo ou globalmente usando uma flag global ou uma função de filtro
-
Também é possível aplicar exceções de eager import apenas a módulos específicos com uma função de filtro
def my_filter(importer, name, fromlist): if name in {'problematic_module'}: return False # eager import return True # lazy import sys.set_lazy_imports_filter(my_filter)
Comportamento em runtime e tratamento de erros
-
Ao usar lazy import, o import real acontece não no ponto da instrução, mas no primeiro acesso ao nome
-
Se o import falhar, o encadeamento de exceções (traceback chaining) mostra com clareza tanto o local da definição quanto o local em que ocorreu o acesso
lazy from json import dumsp # erro de digitação result = dumsp({"key": "value"}) # ImportError ocorre no momento do acesso real
Benefícios de memória e desempenho
- Módulos adiados aparecem apenas no conjunto sys.lazy_modules e não são registrados em sys.modules antes do uso real
- Depois do uso, eles são substituídos por objetos de módulo normais e podem ser usados sem penalidade adicional de desempenho
- Em cargas de trabalho reais, observou-se redução de 50% a 70% na latência de inicialização e economia de 30% a 40% de memória
Resumo do funcionamento
- No primeiro acesso ao objeto lazy ocorre a reification (import real e substituição)
- Quando um código externo acessa o
__dict__do módulo, todos os objetos lazy são carregados à força (reification) - Ao extrair o dicionário com
globals(), o proxy lazy é mantido, exigindo acesso direto
Otimização para type annotations e TYPE_CHECKING
- Com
lazy from módulo import nome, imports usados apenas para tipos têm custo ZERO em runtime garantido - Isso substitui o padrão existente com
from typing import TYPE_CHECKING, deixando o código mais conciso e claro
Diferenças em relação ao PEP 690 e características da implementação
- O PEP 810 segue uma estrutura opt-in explícita, por import individual e baseada em objeto proxy simples
- Já o PEP 690 tinha uma estrutura global e implícita de lazy import
Cuidados e interação entre módulos
- Star import (
*) não é suportado com lazy (é sempre eager) - Hooks e loaders de import personalizados continuam funcionando normalmente no momento da reification
- Mesmo em ambiente multithread, há garantia de import thread-safe, feito apenas uma vez e com binding seguro
- Se o mesmo módulo for usado ao mesmo tempo como lazy e eager, o eager sempre tem prioridade
Guia de aplicação no código e migração
- Ao aplicar isso em código existente, recomenda-se converter para lazy apenas os imports necessários com base em profiling, em uma adoção gradual
- Ao usar
__lazy_modules__, há compatibilidade também com versões anteriores ao Python 3.15
Outros pontos importantes de perguntas e respostas
- Efeitos colaterais em tempo de import (por exemplo, padrões de registro) também são adiados até o primeiro acesso. Se o side effect for essencial, recomenda-se um padrão com função de inicialização explícita
- O problema de circular import não é resolvido completamente com lazy import (só pode ser amenizado se o acesso também for adiado)
- O desempenho em hot paths elimina completamente a checagem lazy após o primeiro uso, com otimização automática (adaptive specialization de bytecode)
- O módulo real só é registrado em
sys.modulesapós a reification (primeiro uso) - Diferentemente de
importlib.util.LazyLoader, não exige configuração separada, mantém o desempenho e oferece clareza com uma sintaxe padrão
Conclusão
- O PEP 810 adiciona a palavra-chave
lazyàs instruções de import do Python, permitindo otimizar de forma simples e previsível problemas de desempenho causados por carregamento desnecessário de módulos em várias áreas, como CLIs com subcomandos, grandes aplicações e type annotations - A nova palavra-chave permite especificar com precisão o momento de adoção e os alvos, sendo adequada para adoção gradual e tuning de performance em serviços reais
- Trata-se de uma evolução prática do sistema de imports do Python, atendendo ao mesmo tempo às três exigências de visibilidade, manutenibilidade e desempenho
1 comentários
Comentários do Hacker News
Minha ferramenta de CLI llm.datasette.io oferece suporte a plugins, mas houve muitas reclamações sobre o tempo de inicialização muito lento até em comandos como
llm --help; ao investigar, descobri que plugins populares importavam por padrão pacotes pesados como pytorch, bloqueando toda a inicialização, então passei a orientar na documentação para autores de plugins que importem dependências apenas dentro das funções, quando necessário (link da documentação), mas seria muito melhor se esse tipo de problema fosse suportado pela própria linguagem PythonDá para implementar isso na ferramenta hoje mesmo (link com explicação), mas essa abordagem se aplica globalmente ao processo inteiro, então se você tornar o import do numpy lento, todos os imports de submódulos dele também ficarão lentos; no fim, se você não precisar do numpy inteiro, talvez ele nunca seja importado, mas o fenômeno de importar módulos parcialmente só no momento em que forem necessários pode acabar espalhado de forma imprevisível durante o runtime; em experimentos adicionais, vi que ao fazer
import foo.bar.baz,fooefoo.barainda são carregados imediatamente, e sófoo.bar.bazfica lazy, o que provavelmente explica em parte o uso de "mostly" no PEP; se eu melhorar mais minha implementação, talvez consiga resolver issoRecomendo primeiro fazer o parsing da linha de comando, para tratar opções como
--helpsem importar nada; execute imports só quando for realmente necessário, ou, dito de outra forma, projete a ferramenta para só importar depois de processar as opções fáceis da linha de comando e ainda restar trabalho a fazerPropostas de lazy import já existiram no passado e a mais recente foi rejeitada em 2022 (link para a discussão); se bem me lembro, lazy import já existe no Cinder, a variante de CPython da Meta, e este PEP também está sendo conduzido por pessoas que trabalharam no Cinder; o foco do debate era "opt-in ou opt-out?", "até onde se aplica?", "deve entrar como flag de build do CPython?" e afins; no fim, disseram que o Steering Council rejeitou por causa da complexidade de dividir o comportamento de import em dois; espero muito que esta proposta passe, porque eu realmente quero usar esse recurso
Gosto especialmente do fato de ser opt-in, incluir aplicação em nível granular e ainda ter uma chave global para desligar tudo; é uma especificação muito bem montada dentro de várias restrições
Eu também espero que essa proposta passe, mas não estou otimista; isso vai quebrar uma enorme quantidade de código e despejar muitos problemas inesperados; instruções
importtêm efeitos colaterais por natureza, e mudar o momento em que eles se aplicam vai fazer muita gente sofrer por muito tempo com bugs de causa obscura; isso não é alarmismo, é uma preocupação real e fundamentada; há um motivo para lazy import ter ficado só na Meta — é o tipo de coisa que talvez só dê para administrar com os recursos que a Meta tem; muita gente está olhando apenas para "pandas, numpy ou meu weird module complicado é lento demais, seria ótimo se ficasse mais rápido", mas acho que pouca gente sequer entende como o sistema de import do Python realmente funciona; há até muita gente apoiando sem nem saber como lazy import é implementado; no PEP 690 há várias desvantagens — por exemplo, quebra código que usa decorators para adicionar funções a um registry central; a biblioteca Dash é um caso representativo, porque conecta interfaces baseadas em JavaScript e callbacks Python via decorators no momento do import, e se o import virar lazy esse frontend simplesmente morre; serviços com muitos usuários podem quebrar de imediato; dizem "mas é opt-in, se não servir é só desligar o lazy import" — e se o import for transitivo? E se for preciso iniciar um processo importante só depois que o frontend estiver completamente inicializado? Quem sabe que efeitos isso terá num ecossistema em que código e bibliotecas de várias pessoas se misturam? Diferente de type hints, isso afeta de fato o comportamento em runtime; a instruçãoimportaparece em praticamente todo código Python relevante, então ao introduzir lazy import você muda fundamentalmente a forma de execução; além disso, há outros casos estranhos mencionados no PEP; é um problema muito mais difícil do que pareceSeria ótimo poder fazer imports com versão explícita, como
import torch==2.6.0+cu124eimport numpy>=1.2.6, e instalar/importar múltiplas versões do mesmo pacote ao mesmo tempo no mesmo ambiente Python; já passou da hora de acabar com o inferno de conda/virtualenv/docker/bazelNão odeio a ideia, mas também não a recebo com grande entusiasmo; desse jeito, parece que quase todo
importvai acabar recebendolazyna frente, exceto alguns poucos casos que realmente precisem ser eager, então o código fica poluído; e como não há plano de tornar isso o comportamento padrão, essa verbosidade vai ficar para sempre; eu preferiria um sistema em que o módulo declarasse opt-in para lazy loading, sem mudar a sintaxe de import; assim, só as bibliotecas grandes precisariam se preocupar com laziness; claro, isso exigiria que o interpretador vasculhasse o sistema de arquivos no momento do import, além de trazer outras desvantagensSe todo mundo usar bastante lazy import sem grandes problemas, então lazy deveria ter sido o padrão, e <i>eager</i> é que deveria ser a palavra-chave opcional; esse tipo de mudança de paradigma não seria inédito em Python: na v2, várias construções criavam listas de forma eager, e na v3 viraram generators, sem maiores problemas
Se existisse uma flag de linha de comando para tornar lazy o import de módulos em todo o Python, eu usaria sem pensar duas vezes; na prática, fora scripts ou código realmente simples, gerar side effects no carregamento do módulo é um padrão que deveria ser evitado a todo custo
Não acho certo deixar que o módulo decida se deve ou não usar lazy loading; só o chamador sabe se precisa de lazy load, então faz sentido que a opção fique no código que faz o import; qualquer módulo pode ser carregado de forma lazy, e mesmo que tenha side effects, o chamador pode querer adiar isso também
Eu queria poder declarar opções de lazy loading por regex no
pyproject.tomlNo passado, surgiram preocupações parecidas sempre que apareceram recursos novos como type hint, walrus, asyncio, dataclasses etc., mas na prática não houve adoção em massa nem substituição total dos padrões existentes; muitos usuários ainda trabalham basicamente com um Python 2.4 modernizado, e mesmo assim continuam produtivos; já funcionou bem por 20 anos, então não acho que haverá grandes problemas
Se tiver interesse, apresento o lazyimp, que implementa lazy import de forma muito conveniente com context manager; normalmente basta envolver a instrução import com um bloco
with, então funciona bem com ferramentas existentes, e se precisar depurar, dá para voltar facilmente ao eager import; ele alteraf_builtinsdo frame via cext, ficando mais poderoso que hooks do importlib; não é perfeito, mas também existem versões thread-safe e com handler global; no começo eu estava receoso, mas agora migrei quase toda a codebase para isso e, na prática, não tive problema nenhum (tirando não cuidar do processamento de registro por módulo), e o ganho de velocidade percebido foi enorme, então estou satisfeitoÉ realmente incômodo que linters Python forcem imports no topo do arquivo; sempre que tento usar a forma óbvia de implementar lazy import, recebo erro de lint; isso vai além de uma simples questão de performance; por exemplo, quando uma biblioteca específica de plataforma é necessária, eu queria importá-la só naquela plataforma, mas se o import no topo for obrigatório, a importação pode falhar de vez
Nesse caso, acho que não tem jeito a não ser corrigir o linter
A maioria dos linters permite ignorar isso com comentários como
#noqa E402Assim, substituí o meta path finder por um wrapper, trocando o loader por
LazyLoader; quando o import é executado, o nome do módulo é inicialmente vinculado a<class 'importlib.util._LazyModule'>, e o módulo real só é carregado quando um atributo é acessado; código de teste:Mas eu não sei exatamente o que significa o "mostly" no PEP
Acho que os riscos de thread safety em lazy import estão sendo subestimados; não dá para prever quando, em qual thread e sob quais locks o import será executado, e fora o lock do importer não há muita garantia; antes, mesmo que código perigoso rodasse no momento do import do módulo, isso quase sempre acontecia só durante a inicialização single-threaded, então raramente virava um grande problema; se isso mudar para lazy, os erros podem aparecer de forma realmente imprevisível, no estilo Heisenbug; imports em nível de função também têm esse risco, mas ao menos existe a previsibilidade de que eles serão executados logo no começo de um trecho explícito de código
Parece um bom recurso; a explicação é simples, os casos de uso reais e o escopo (global, palavra-chave simples) parecem adequados, gostei
Entre os PEPs recentes, este me parece um dos mais limpos do ponto de vista do usuário; depois de passar por esse tradicional processo de bikeshedding sintático, estou curioso pelo resultado final
Acho que é um PEP preparado com muito cuidado: validação em casos reais de trabalho e edge cases, compromissos apropriados, abordagem sem exageros, várias rodadas de refinamento; especialmente porque mexer no sistema ósseo central de uma linguagem grande com comunidades tão diferentes no mundo todo pode ser muito arriscado, então isso me impressiona ainda mais
Espero que tenham aprendido bem por que o PEP-690 foi rejeitado; na nossa codebase também tentamos implementar algo assim por conta própria, mas nunca conseguimos fazer funcionar bem o suficiente para valer a pena
O perigo do lazy import é que ele facilita a criação de erros inesperados em runtime em serviços de longa duração; parece uma grande vantagem para inicialização rápida, mas o tradeoff é aceitar a possibilidade de a execução parar no meio por falha de import; além disso, podem surgir edge cases em que já não seja possível garantir no início do programa quais coisas ainda serão importadas depois
Ainda assim, esse é um problema real que precisa ser resolvido; não é só uma questão de velocidade de startup, é que a inicialização do Python fica absurdamente lenta quando entram dependências grandes; projetos maiores não podem simplesmente embutir todas as bibliotecas pesadas que nem todos os usuários usam, então os desenvolvedores já recorrem a gambiarras ainda mais bizarras, que por sua vez trazem outros problemas absurdos; só de eliminar a necessidade de esconder ou duplicar imports em nível de função já seria um grande avanço; e isso está sendo proposto apenas como um recurso opcional da linguagem
Testes automatizados já conseguem mitigar bastante o risco, e isso vale a pena em troca de uma inicialização rápida; tempo de startup nunca é só uma questão de "aparência"; num monólito Django, eu já vivi situações em que, por causa de apenas algumas bibliotecas pesadas, era preciso esperar 10 a 15 segundos em todo management command, teste e recarregamento de contêiner; adiar esses imports com lazy fez uma diferença enorme
Nós tendemos a preferir imports explícitos no topo do arquivo, justamente para expor problemas de dependência logo no início do programa; com lazy import, você ganha a inconveniência de só descobrir o problema quando um caminho específico de código for executado (talvez horas ou dias depois)
A maior parte desse tempo vai para importar e descarregar módulos vendorizados que nem chegam a ser usados de fato (por exemplo, só os módulos relacionados a Requests já somam quase 100), e ao investigar vi que mais de 500 módulos estão sendo importados desnecessariamente
Também não entendo por que geradores de código estão produzindo tanto código com
local importdentro de funções em vez de imports no topo; eu não gostaria de incentivar esse padrão, porque fica mais difícil entender as dependências do módulo e aumenta o risco de dependências cíclicas depoisAinda não li o PEP inteiro, mas acho que seria bom se houvesse algum tipo de validação de dependências por flag de linha de comando ou ferramenta externa, como existem ferramentas para type hints
Fiquei curioso sobre quem exatamente está incluído nesse "nós"
Não seria esse um problema que deveria ser coberto por testes?