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
Continue lendo
Desenvolvimento Go 1.27: encoding/json/v2 muda os padrões e o mesmo struct passa a gerar outro JSON
Go 1.27 tornou encoding/json/v2 estável: campo desconhecido e nome duplicado viram erro, slice nil vira [], o match de campos é case-sensitive e o UTF-8 inválido é rejeitado. Veja o que quebra, as opções que restauram o v1 e como migrar por partes.
Ler artigo →
Desenvolvimento TypeScript 7: compilador em Go é 10x mais rápido, mas a migração trava é nas ferramentas
TypeScript 7.0 trocou o compilador por uma versão em Go e levou junto a API que alimenta linters, loaders e codegen. A 7.1 beta devolve parte dela com entry points separados e content mappers. Entenda o que quebra, como rodar 6 e 7 lado a lado e o que o ngp do Angular revela.
Ler artigo →
Desenvolvimento Backdoor SSH no Claude Code do Cursor e CNHs da Uber: a chave vazada da Fireworks vira o elo da cadeia de IA
Cursor fecha investigação: chave vazada da Fireworks permitiu trocar o Claude Code por versão com backdoor SSH para até 5 mil usuários. A mesma origem é apontada no vazamento de CNHs de motoristas da Uber. No mesmo ciclo, a Europol apreendeu 235 servidores de Gootloader, CastleRAT e StealC.
Ler artigo →