Shopping cart

Subtotal $0.00

View cartCheckout

Building better devs

TnewsTnews
Programação

Python 3.15: 9 Features Escondidas Que Ninguém Está Falando

Email : 13

Quem acompanha o ciclo de releases do Python sabe que a cada versão nova, todo mundo fala das mesmas três coisas — e ignora completamente as features que realmente vão impactar o dia a dia de quem escreve código. Com o Python 3.15 entrando em feature freeze em maio de 2026 e beta 1 já disponível, é hora de cavar além do óbvio.

A real é que o 3.15 é possivelmente a release mais ambiciosa desde o 3.10. Lazy imports, um tipo frozendict nativo, unpacking em comprehensions, um profiler que roda a 1 milhão de amostras por segundo, thread-safe iterators… a lista é absurda. E eu aposto que você não ouviu falar de pelo menos metade disso.

Lazy Imports: Seu Script Vai Iniciar em Milissegundos

A PEP 810 introduz a keyword lazy para imports. Na prática, o módulo só é carregado quando você realmente usa ele pela primeira vez.

lazy import json
lazy from pathlib import Path

print("Iniciando...")  # json e pathlib ainda NÃO foram carregados
data = json.loads('{"chave": "valor"}')  # json carrega aqui

Parece simples, mas o impacto é brutal. Aplicações com dezenas de imports pesados — pense em Django, FastAPI com SQLAlchemy, ou qualquer CLI com click + rich + httpx — vão sentir a diferença imediatamente.

Você também pode ativar lazy imports globalmente via linha de comando:

python -X lazy_imports=all meu_script.py

Ou via variável de ambiente:

PYTHON_LAZY_IMPORTS=all python meu_script.py

Existe até controle programático com sys.set_lazy_imports() e filtros por módulo. Mas atenção: lazy imports funcionam apenas no escopo do módulo. Nada de usar dentro de funções, e star imports (from x import *) continuam proibidos.

Para quem mantém bibliotecas, existe o __lazy_modules__ — uma lista no nível do módulo para garantir backward compatibility com ferramentas que dependem de imports serem executados imediatamente.

MétodoEscopoExemplo
lazy importPor importlazy import pandas
-X lazy_imports=allGlobal (CLI)python -X lazy_imports=all app.py
PYTHON_LAZY_IMPORTSGlobal (env)Ideal para Docker/CI
sys.set_lazy_imports()ProgramáticoControle fino em runtime

frozendict: Dicionários Imutáveis Finalmente São Nativos

A comunidade pediu isso por mais de uma década. A PEP 814 traz o frozendict como tipo built-in — sem precisar de bibliotecas externas.

config = frozendict(host="localhost", port=5432, db="prod")

# Tentou alterar? TypeError.
config["host"] = "remoto"  # TypeError: 'frozendict' object does not support item assignment

# E o melhor: é hashable
cache = {config: "conexão_pool_1"}

O frozendict preserva ordem de inserção, mas comparação ignora a ordem (assim como sets). Dois frozendicts com os mesmos pares chave-valor são iguais e geram o mesmo hash, independente da ordem em que foram criados.

A integração com json ficou especialmente elegante:

import json

dados = json.loads(
    '{"usuario": {"nome": "Ana", "tags": ["dev", "python"]}}',
    object_hook=frozendict,
    array_hook=tuple
)

# dados é completamente imutável e hashable
print(type(dados))           # <class 'frozendict'>
print(type(dados["tags"]))   # <class 'tuple'>

Já funcionam com pickle, copy, marshal, pprint, xml.etree e mais. Se você já usou types.MappingProxyType como gambiarra para ter dicts “imutáveis”, pode aposentar essa abordagem.

Unpacking em Comprehensions: O Açúcar Sintático que Faltava

A PEP 798 é daquelas features que fazem você pensar “como isso não existia antes?”. Agora você pode usar * e ** em list, set e dict comprehensions.

listas = [[1, 2], [3, 4], [5]]
resultado = [*L for L in listas]
# [1, 2, 3, 4, 5]

Antes, você precisava de itertools.chain.from_iterable() ou um loop aninhado. Agora é uma linha.

Para dicts:

configs = [{"debug": True}, {"port": 8080}, {"debug": False}]
merged = {**d for d in configs}
# {"debug": False, "port": 8080}

E sets:

grupos = [{1, 2}, {2, 3}, {3, 4}]
uniao = {*s for s in grupos}
# {1, 2, 3, 4}

Funciona inclusive com generators e async generators:

gen = (*L for L in listas)  # generator que produz itens "desempacotados"

# Async
async def combinar():
    return [*item async for item in async_gen()]

Eu já consigo ver esse pattern substituindo 90% dos casos onde as pessoas usam itertools.chain ou loops explícitos. É mais legível, mais Pythonic, e o interpretador pode otimizar melhor.

Tachyon: Um Profiler de 1 Milhão de Amostras por Segundo

A PEP 799 reorganizou o módulo de profiling e adicionou algo que muda o jogo: o Tachyon, um sampling profiler com overhead virtualmente zero.

# Profiling de um script
python -m profiling.sampling run meu_app.py

# Attach em um processo rodando (sem reiniciar!)
python -m profiling.sampling attach --pid 12345

# Gerar flamegraph
python -m profiling.sampling run --flamegraph output.svg meu_app.py

O Tachyon suporta até 1.000.000 Hz de amostragem. Para comparação, o cProfile tradicional instrumenta cada chamada de função, adicionando overhead significativo. O Tachyon faz amostragem estatística — tira “fotos” do stack em intervalos regulares — com custo perto de zero.

FeaturecProfileTachyon
TipoDeterminísticoAmostragem
OverheadAltoQuase zero
ModoInstrumentaçãoEstatístico
Max HzN/A1.000.000
ProduçãoNão recomendadoSim
Attach em processoNãoSim

Os modos de profiling são impressionantes:

  • Wall-clock time — tempo real total
  • CPU time — só tempo de CPU (ignora I/O wait)
  • GIL-holding time — quanto tempo seu código segurou o GIL
  • Exception handling time — tempo gasto tratando exceções

O modo GIL-holding é ouro para quem tenta otimizar código multi-threaded. E o suporte a async com --async-aware mostra corretamente onde suas coroutines estão esperando.

# Profiling de tempo de GIL em todos os threads
python -m profiling.sampling run -a --mode gil meu_servidor.py

# Saída em formato compatível com speedscope
python -m profiling.sampling run --gecko profile.json meu_app.py

O cProfile continua funcionando como alias, mas o módulo profile (puro Python) foi marcado para remoção no 3.17. Se você ainda usa, é hora de migrar.

Thread-Safe Iterators: Chega de Queue para Tudo

Três novas utilidades no módulo threading resolvem um problema que todo mundo que fez código multi-threaded em Python já enfrentou:

import threading

# Serializa acesso a um iterator entre threads
eventos = threading.serialize_iterator(stream_events())

# Cada thread pode chamar next() com segurança
# sem race conditions

# Ou crie cópias independentes para consumo paralelo
copia1, copia2 = threading.concurrent_tee(gerador_dados(), n=2)

O serialize_iterator() garante que chamadas a next() de threads diferentes sejam serializadas. O synchronized_iterator() funciona como decorator. E o concurrent_tee() é um itertools.tee() que funciona corretamente com threads.

Antes, a solução padrão era converter tudo para Queue, o que exige refatoração significativa. Agora você envolve o iterator existente e pronto. Nenhuma mudança de arquitetura necessária.

asyncio.TaskGroup.cancel(): Cancelamento Limpo

Se você já usou TaskGroup do asyncio, sabe que cancelar tasks no meio da execução era um pesadelo de contextlib.suppress(asyncio.CancelledError):

# Python 3.14 — a gambiarra
async with asyncio.TaskGroup() as tg:
    tg.create_task(baixar_arquivo("a.zip"))
    tg.create_task(baixar_arquivo("b.zip"))
    # Para cancelar? Boa sorte.

# Python 3.15 — limpo
async with asyncio.TaskGroup() as tg:
    tg.create_task(baixar_arquivo("a.zip"))
    tg.create_task(baixar_arquivo("b.zip"))

    if await sinal_de_parada():
        tg.cancel()  # Cancela tudo gracefully

Sem exceções customizadas, sem suppress, sem boilerplate. Uma linha.

UTF-8 Padrão: Vai Quebrar Coisas (E Tá Certo)

A PEP 686 finalmente faz o que deveria ter sido feito há 15 anos: UTF-8 é o encoding padrão para I/O em todas as plataformas.

O que muda na prática: open("arquivo.txt") sem encoding= agora assume UTF-8, não o encoding do sistema operacional. No Windows, isso significava CP-1252 ou similar. No Linux, geralmente já era UTF-8 — mas não sempre.

# Python 3.14
open("dados.txt")  # encoding = locale do SO (pode ser cp1252, latin-1, etc.)

# Python 3.15
open("dados.txt")  # encoding = utf-8, SEMPRE

Se você precisa do comportamento antigo:

open("dados.txt", encoding="locale")  # usa o encoding do sistema

Ou desative globalmente: PYTHONUTF8=0 ou python -X utf8=0.

Quem já seguia a recomendação de sempre passar encoding= nos open() não vai sentir nada. Quem não seguia… bom, agora tem um motivo concreto para auditar o codebase.

Mensagens de Erro que Leem Sua Mente

Python já vinha melhorando as mensagens de erro, mas o 3.15 vai além:

class Container:
    def __init__(self):
        self.inner = Shape(area=42)

c = Container()
c.area
# AttributeError: 'Container' object has no attribute 'area'.
# Did you mean '.inner.area'?

O interpretador agora navega atributos internos para sugerir o caminho correto. Mas a melhoria mais legal é para quem vem de outras linguagens:

minha_lista = [1, 2, 3]
minha_lista.push(4)
# AttributeError: 'list' object has no attribute 'push'.
# Did you mean '.append()'?

E para quem tenta mutar tipos imutáveis:

minha_tupla = (1, 2, 3)
minha_tupla.append(4)
# AttributeError: 'tuple' object has no attribute 'append'.
# Consider using 'list' if you need a mutable sequence.

Parece detalhe, mas para quem ensina Python ou faz onboarding de devs que vêm de JavaScript/Java, essas mensagens economizam horas de Stack Overflow.

frozendict + json: Parsing Imutável em Uma Linha

Voltando ao frozendict por um segundo — a integração com json merece destaque próprio porque resolve um pattern que aparece em quase todo projeto:

import json
from functools import lru_cache

# Antes: impossível cachear resultados de JSON parsing
# porque dicts não são hashable

# Agora:
config_str = '{"db": {"host": "localhost", "port": 5432}, "features": ["auth", "cache"]}'

config = json.loads(config_str, object_hook=frozendict, array_hook=tuple)

# config é 100% imutável e hashable
# Pode ser chave de dict, elemento de set, argumento de lru_cache
@lru_cache
def get_connection(cfg):
    return create_pool(cfg["db"]["host"], cfg["db"]["port"])

conn = get_connection(config)  # funciona!

Isso abre portas para caching agressivo de configurações, memoização de resultados de API, e qualquer cenário onde você precisa usar dados JSON como chave. Antes, a gambiarra era converter tudo para tuple de tuple — feio e propenso a erros.

O frozendict também funciona como default em argumentos de função sem o bug clássico do mutable default:

def conectar(opcoes=frozendict(timeout=30, retries=3)):
    # Seguro! Ninguém vai mutar o default acidentalmente
    ...

JIT Compiler: 12% Mais Rápido e Ninguém Notou

O JIT do CPython, que estreou como experimental no 3.13, agora entrega ganhos reais: 8-9% de performance média no x86-64 Linux e 12-13% no Apple Silicon (macOS) comparado ao interpretador padrão.

Os binários oficiais do Windows 64-bit agora usam o tail-calling interpreter por padrão. Não é a revolução que PyPy prometia, mas é melhoria consistente rodando CPython puro, sem trocar de runtime.

E uma nota importante: o garbage collector incremental do 3.14 foi revertido para o modelo geracional clássico. Tinha memory leaks que não foram resolvidos a tempo. Pode voltar no 3.16 via PEP formal, mas por ora, voltamos ao que funcionava.

As “Pequenas” que Fazem Diferença

Algumas features menores que merecem menção:

bytearray.take_bytes() extrai bytes sem cópia. Brutal para parsing de protocolos e buffers de rede:

buffer = bytearray(1024)
# ... preenche o buffer ...
dados = buffer.take_bytes()  # zero-copy, buffer fica vazio

Counter XORcollections.Counter agora suporta o operador ^ para diferença simétrica:

from collections import Counter
a = Counter(x=3, y=1)
b = Counter(x=1, y=1)
print(a ^ b)  # Counter({'x': 2})

TOML 1.1.0 — o tomllib agora aceita trailing commas em inline tables, \xHH em strings, e segundos opcionais em datetime. Chega de “TOML estrito demais”.

re.prefixmatch() — o re.match() foi soft-deprecated porque confundia devs que esperavam match completo. Agora re.prefixmatch() deixa explícito que é match do início da string, e re.fullmatch() continua para match completo.

subprocess.Popen.wait() eficiente — no Linux 5.3+, usa pidfd_open() + poll() ao invés de busy loop. No macOS, usa kqueue. Performance real em scripts que esperam processos.

os.makedirs() com parent_mode — finalmente dá para criar diretórios intermediários com permissões diferentes do diretório final:

import os
os.makedirs("/app/data/uploads", mode=0o755, parent_mode=0o700)
# /app e /app/data criados com 700, /app/data/uploads com 755

Unpacking em comprehensions + frozendict — as features combinam bem:

# Merge de múltiplas configs imutáveis
configs = [frozendict(a=1), frozendict(b=2), frozendict(a=3)]
merged = frozendict({**d for d in configs})
# frozendict({'a': 3, 'b': 2}) — imutável

unicodedata.iter_graphemes() — itera por clusters de grafemas (UAX #29). Essencial para quem trabalha com emojis ou texto multi-script. len("👨‍👩‍👧‍👦") continua retornando 7, mas list(unicodedata.iter_graphemes("👨‍👩‍👧‍👦")) retorna 1 cluster.

Quem quer testar agora, o beta 1 já está disponível. Instale com pyenv install 3.15.0b1 ou baixe direto do python.org. A release final tá marcada para 1 de outubro de 2026 — e pelo volume de features, vale começar a testar a compatibilidade dos seus projetos agora, não em setembro. A real é que cada vez menos gente lê o changelog inteiro, e cada vez mais gente descobre feature nova por tweet aleatório seis meses depois. Não seja essa pessoa.

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts