Fale conosco
Desenvolvimento

Python 3.15: lazy import adia o erro para a primeira requisição — e o UTF-8 virou padrão

38 min de leitura
Python 3.15: lazy import adia o erro para a primeira requisição — e o UTF-8 virou padrão

O Python 3.15 saiu em 9 de outubro de 2026 e já está em manutenção de bugfix, com 3.15.1 (21/10) e 3.15.2 (26/10) publicadas. Entre as mudanças da versão, duas têm impacto direto em código que já está em produção e que quase ninguém discute em release note: o lazy import explícito (PEP 810) e o UTF-8 como encoding padrão (PEP 686).

O ponto não é "Python ficou mais rápido". É que o lazy muda onde e quando o seu programa falha. Um ImportError que antes derrubava o processo no boot passa a aparecer na primeira requisição que toca aquele nome — depois do health check ter passado, do deploy ter ficado verde e do tráfego ter chegado. Junto com isso, módulos que registram coisas via decorator quando são importados deixam de registrar até alguém usar o nome.

Para quem mantém API, CLI ou worker em Python, isso é uma mudança de contrato de falha, não só de tempo de inicialização. Vale entender antes de sair aplicando lazy em todo import.

O que o lazy realmente adia

lazy é uma soft keyword : só tem significado na frente de um import . A forma mais comum de uso é no topo do arquivo:

O nome pandas existe imediatamente, mas aponta para um placeholder. O corpo do módulo só roda na primeira operação que realmente precisa dele — atributo acessado, função chamada. Se o caminho de execução nunca usar pandas , você nunca paga o custo. Também existe a forma lazy from modulo import nome .

O ganho real depende de onde você aplica. Medições publicadas variam bastante: um CLI que carregava bibliotecas de IA caiu de ~2,5 s para ~14 ms de startup marcando apenas os imports que não eram usados no caminho --help ; um servidor MCP caiu de 1,6 s para 0,6 s. Os números de 50% a 70% citados no PEP vêm do modo global, não do uso pontual do keyword — essa distinção importa para não prometer o que a ferramenta não entrega.

Medindo antes de mexer

A ferramenta de diagnóstico continua sendo a mesma:

Ordene pelo tempo cumulativo e marque como lazy só o que aparece no caminho que não é o principal. Bibliotecas grandes de CLI, SDKs de nuvem, matplotlib , openpyxl e afins são candidatos naturais; o framework HTTP da sua API, quase nunca.

Onde quebra de verdade

1. Erro de importação vira erro de requisição. Se uma dependência não está no ambiente, o processo sobe e o ImportError (ou ModuleNotFoundError ) aparece no meio do primeiro request que usa o nome. A mitigação é ter um check de boot que force a carga de tudo que é crítico:

Cuidado: importar módulos em massa executa efeitos colaterais. Se o seu pacote registra handlers ou abre conexões no import, prefira uma lista explícita de módulos obrigatórios.

2. Registry de plugin fica vazio. O padrão @register("pdf") só funciona porque o import executa o decorator. Com lazy , o registry nasce vazio e a primeira chamada devolve "nenhum handler".

Regra prática: módulos de registro e de wiring devem continuar com import eager. lazy é para folhas da árvore de dependências, não para o mecanismo de descoberta.

3. Testes com mock.patch passam a resolver o proxy. Qualquer acesso ao atributo — patch("servico.requests") , hasattr , dir() — dispara o carregamento. Isso funciona, mas o teste deixa de medir o que você achava que media, e patches aplicados antes da resolução do proxy podem ficar sem efeito.

4. Warnings mudam de linha. Um warnings.warn(..., stacklevel=2) no nível do módulo passa a apontar para o primeiro uso, não para a linha do import . Diffs de saída de CLI ficam estranhos, e filtros por localização param de casar.

try: continua eager, with não — e none não existe mais

Duas pegadinhas de especificação, confirmadas em 3.15.2:

Import dentro de try: permanece eager. O idioma try: import fast except ImportError: import fallback continua funcionando (inclusive o modo global de imports preguiçosos).

Import dentro de with: vira lazy. Ou seja, o corpo do módulo pode rodar depois que o context manager já saiu, o que pode ser surpresa em código que configura logging ou conexões ali dentro.

E atenção ao modo global: PEP 810 documenta três valores para -X lazy_imports / PYTHON_LAZY_IMPORTS / sys.set_lazy_imports() — normal , all e none . O valor none foi removido antes do 3.15.0 (gh-149321): hoje -X lazy_imports=none é erro fatal de startup, mesmo com a PEP ainda listando o valor. Se o seu runbook de diagnóstico usa essa flag, ele já está quebrado. O modo all também não é gratuito: ele já quebrou pandas em testes públicos, porque assume que qualquer import pode ser adiado.

UTF-8 como padrão: o CSV que "só funcionava no Windows"

PEP 686 torna o modo UTF-8 o padrão. open() sem encoding= , subprocess com text=True e locale.getpreferredencoding(False) deixam de herdar a codificação do sistema — que no Windows em português era cp1252.

Um serviço de exportação que funcionava há anos passou a gerar Renée,Montréal no CSV depois do upgrade, sem exceção, sem log, sem teste vermelho. O arquivo estava "correto" em UTF-8; o consumidor é que esperava Latin-1. Do outro lado, código que já forçava encoding="utf-8" não mudou nada — e essa é justamente a lição: explicitar encoding sempre foi a solução e agora passou a ser obrigatório na prática.

Traps concretos: saída de ferramentas nativas do Windows decodificada como UTF-8 pode estourar UnicodeDecodeError ; nome de arquivo lido de API externa e gravado no disco muda de bytes; e-mail e geração de PDF com fontes legadas idem. Para fazer bisect ou reproduzir o comportamento antigo, PYTHONUTF8=0 ainda funciona. Plataformas gerenciadas também estão virando o default sem você tocar em nada: o Vercel Sandbox troca a imagem padrão para 3.15 em 16 de dezembro de 2026 — se o seu job roda ali, fixe a versão da imagem ou teste agora.

Riscos e custo de adoção

Adoção de lazy é fácil; a verificação é o trabalho. Sem um check que force os imports críticos, você troca um erro de boot por um 500 intermitente.

lazy aplicado em módulos de registro quebra funcionalidade silenciosamente — o teste mais valioso é o de integração que sobe a aplicação de verdade.

O modo global all é atrativo e perigoso: ganho maior, superfície de falha muito maior, e bibliotecas que assumem execução de import no load param de funcionar.

UTF-8 exige auditoria de I/O, não só de str . Meça com testes que comparem bytes de saída, não apenas conteúdo decodificado.

Ordem de migração recomendada: UTF-8 primeiro (é onde há incidente silencioso de dados), lazy depois, e por último frozendict , sentinel e o profiler de amostragem Tachyon — este último exige código novo só se você já caça gargalo em produção.

Recomendação prática

Trate 3.15 como uma migração de duas etapas. Na primeira, suba a versão sem mudar código: rode a suíte, compare arquivos gerados byte a byte e verifique os pontos onde o encoding era implícito. Na segunda, marque lazy apenas em imports folha, medindo com -X importtime antes e depois, e mantenha um gate de CI que carregue tudo que é obrigatório. Em times com dezenas de serviços, o esforço maior não é aplicar a mudança — é mapear quais módulos são wiring e quais são folha.

Em projetos que a Maquinaweb mantém em Python e Node.js, esse mapeamento virou parte do checklist de upgrade de runtime: antes de subir a versão, o time identifica imports de registro, pontos de I/O sem encoding explícito e depende de teste de integração para validar o boot — não da leitura do changelog.

Se o seu time está decidindo quando migrar, ou precisa revisar uma base grande antes de trocar de versão, vale conversar com a Maquinaweb sobre o desenho dessa migração e do desenvolvimento sob medida que vem depois.

Fontes

#Python #Python 3.15 #lazy imports #PEP 810 #UTF-8 #encoding #migração #backend #boas práticas
Contato

Bora conversar?

Três campos. A resposta vem no WhatsApp.

Ou escreva para contato@maquinaweb.com.br.

Você vai para o WhatsApp com a mensagem pronta. Sem spam, sem lista.

Seus dados seguem a nossa política de privacidade.