Python 3.15.0 se publicó el 9 de octubre de 2026. Sus cambios principales caen justo donde más duelen las apps LLM: lazy imports explícitos (PEP 810) para el arranque del proceso, UTF-8 como encoding por defecto (PEP 686) para archivos y pipes, los nuevos tipos built-in sentinel y frozendict, y un profiler de muestreo (PEP 799).
Leí el What’s New, los PEPs y la metadata de PyPI para que decidas rápido: qué cambia, qué soporta hoy el ecosistema y un plan de una semana. No medí 3.15 yo mismo, así que cada número de abajo está atribuido a su fuente.
1. Contexto: Qué Salió el 9 de Octubre de 2026
3.15.0 llegó en la fecha del calendario de lanzamiento (PEP 790) después de tres release candidates, y python.org lo marca como la versión estable. La documentación dice que 3.15 soporta Windows 10 y posteriores, y que los binarios oficiales de Windows de 64 bits ahora usan el intérprete con tail calls.
Calendario de lanzamiento (PEP 790)
Dos novedades destacadas que podés hojear
El PEP 803 agrega abi3t, una ABI estable para builds free-threaded: concierne a autores de extensiones en C y a herramientas de wheels, no al código de aplicación, aunque las dependencias compiladas podrían empezar a publicar wheels abi3t. El JIT sigue siendo experimental: el What’s New reporta una mejora de media geométrica de 7–8% en pyperformance sobre el intérprete estándar en x86-64 Linux, con benchmarks individuales entre una caída de ~15% y una mejora de más de 100% frente a builds sin JIT. Son cifras de la suite de benchmarks de CPython, no de cargas de agentes que casi todo el tiempo esperan a la red.
Consecuencia práctica: lazy imports, sentinel, frozendict y el profiler los adoptás a tu ritmo. UTF-8 es distinto — aplica en cuanto tu código existente corre en 3.15, por eso tiene una sección completa más abajo.
2. Cuatro Implicaciones para Devs de LLM y Agentes
Estos son los cuatro cambios que llegan al código de aplicación. Los lazy imports son la mayor palanca de arranque; UTF-8 es el que puede cambiar el comportamiento sin que lo actives.
Lazy imports para el arranque en frío
PEP 810 · Opt-in · Solo a nivel de módulo
lazy import openai enlaza un proxy y carga el módulo en el primer uso, así --help o un subcomando corto dejan de pagar cada SDK importado al inicio del archivo. La ganancia depende de tu grafo de imports; medí con -X importtime.
UTF-8 por defecto
PEP 686 · No es opt-in · Sobre todo Windows
open(), TextIOWrapper y los pipes de subprocess en modo texto sin encoding explícito ahora usan UTF-8 sin importar el locale. En Linux y macOS suele ser lo que ya tenías; en Windows reemplaza la code page ANSI.
sentinel y frozendict
PEP 661 · PEP 814 · Built-ins
sentinel('MISSING') es un marcador único con repr legible que sobrevive a copy; separa “el modelo omitió el argumento” de “el modelo mandó null”. frozendict es un mapeo inmutable, hasheable cuando todas sus claves y valores lo son, para schemas y config.
Profiler de muestreo Tachyon
PEP 799 · profiling.sampling
Te conectás a un proceso en ejecución por PID y obtenés un flame graph sin tocar el código. El modo wall muestra dónde espera un loop de agente, cpu dónde gasta CPU, y --async-aware sigue las cadenas de await.
Antes y después: una CLI de agente con imports pesados
Una CLI de agente típica importa un SDK, una librería de arrays y un tokenizador al inicio del archivo y después usa cada uno en un solo subcomando. Con el PEP 810 ponés la soft keyword lazy delante del import: el nombre queda enlazado a un proxy liviano y el módulo se carga en el primer uso. La sintaxis de abajo es la de la referencia del lenguaje. No hay tiempos porque no medí tu carga de trabajo — el siguiente bloque te muestra cómo lo vas a medir vos.
# agent_cli.py — Python 3.14 y anteriores: todo se carga antes de que corra main()
import argparse
import json
import numpy as np # solo lo necesita el subcomando `embed`
import tiktoken # solo lo necesita `count`
from openai import OpenAI
# agent_cli.py — Python 3.15 (PEP 810): primero proxies, módulos reales en el primer uso
import argparse
import json
lazy import numpy as np
lazy import tiktoken
lazy from openai import OpenAI
def main():
args = build_parser().parse_args() # --help nunca carga los tres pesados
if args.cmd == 'embed':
vectors = np.asarray(load_vectors(args.path)) # numpy se carga acáEl grafo de imports de --help
Nada más cambia en el archivo. Los nombres se enlazan al instante; el costo se mueve a la primera línea que realmente los usa.
Antes (eager)
agent_cli.py --help ├─ argparse, json ├─ numpy cargado ├─ tiktoken cargado └─ openai cargado └─ todo su árbol de deps
Después (lazy import)
agent_cli.py --help ├─ argparse, json ├─ numpy solo proxy ├─ tiktoken solo proxy └─ openai solo proxy agent_cli.py embed └─ numpy carga al primer uso
Medilo, no le creas
python -X importtime imprime en stderr el tiempo propio y acumulado de cada import, y -X importtime=2 (3.14+) también lista los módulos que ya estaban cargados, marcados como cached. Corrélo antes y después sobre los comandos que más usan tus usuarios. La motivación del PEP dice que la laziness puede recortar el arranque un 50–70% en CLIs pero no cita fuente para esa cifra; el único número atribuido en sus casos de estudio es 10–20% para aplicaciones PySide. Tu resultado depende por completo de qué importás y qué corre al importar.
# el costo por import (propio y acumulado, en microsegundos) va a stderr
python -X importtime agent_cli.py --help 2> importtime.txt
# top 15 por tiempo acumulado (segunda columna)
sort -t'|' -k2 -n -r importtime.txt | head -15
# listar también los módulos que ya estaban cargados, marcados `cached`
python -X importtime=2 agent_cli.py --help 2> importtime2.txt
# comprobá la laziness en vez de suponerla (esperá False cuando el import es lazy)
python -c "import sys, agent_cli; print('openai' in sys.modules)"Ir más allá de un import a la vez
Hay dos perillas. __lazy_modules__ hace lazy los import normales de los módulos listados en 3.15+ y las versiones viejas lo ignoran, lo que sirve a librerías y a flotas mixtas 3.14/3.15. El modo global (-X lazy_imports=all, PYTHON_LAZY_IMPORTS=all o sys.set_lazy_imports) vuelve potencialmente lazy todo import a nivel de módulo; la doc de sys dice que los autores de librerías en general no deberían llamarlo. Un filtro limita el radio de impacto a tu propio paquete.
# Opción A: un solo árbol de fuentes para 3.14 y 3.15 (las versiones viejas ignoran la lista)
__lazy_modules__ = ['numpy', 'tiktoken']
import numpy
import tiktoken
# Opción B: pasar el proceso a lazy, pero solo para tu propio paquete
import sys
def only_mine(importing, imported, fromlist):
return imported.startswith('myagent.') # True = puede ser lazy, False = fuerza eager
sys.set_lazy_imports_filter(only_mine)
sys.set_lazy_imports('all') # el mismo interruptor que -X lazy_imports=all
# Opción C: sin tocar código, todo el proceso
# python -X lazy_imports=all agent_cli.py
# PYTHON_LAZY_IMPORTS=all python agent_cli.pyQué pueden romper los lazy imports
Los efectos secundarios pasan del momento del import al primer uso: registros con decoradores, hooks de plugins y patrones con __init_subclass__ pueden dejar de registrarse hasta que algo toque el módulo. Los errores de import también se mueven — una instalación rota falla a mitad de la ejecución en vez de al arrancar. lazy solo es válido a nivel de módulo (SyntaxError dentro de funciones, clases y bloques try), los imports con estrella siguen siendo eager y los imports circulares no se resuelven por arte de magia. Linters y formateadores tienen que aprender la keyword, como dice el propio PEP, así que pasá un lazy import por tu toolchain antes de desplegarlo.
3. Los Otros Tres: UTF-8, sentinel/frozendict y Tachyon
Mismo método: qué dice la documentación y después el código que escribiría.
UTF-8 por defecto: igual pasá el encoding explícito
El PEP 686 activa el modo UTF-8 por defecto. La documentación dice que aplica cuando no se da un argumento encoding y recomienda pasarlo siempre para ser compatible entre versiones. En Windows, Python usaba por defecto la code page ANSI para archivos de texto; con el modo UTF-8 activo la ignora y usa UTF-8, y la code page sigue disponible con el codec mbcs. La E/S de consola y el encoding del sistema de archivos ya eran UTF-8 en Windows (PEP 528 y 529).
from pathlib import Path
import subprocess
# 1. Archivos: decí lo que querés, en cualquier SO y versión de Python
Path('memory.jsonl').write_text(payload, encoding='utf-8')
with open('legacy_export.csv', encoding='locale') as f: # la code page del sistema, a propósito
rows = f.read()
# 2. Pipes: Python decodifica, el proceso hijo decide qué bytes escribe
r = subprocess.run(['git', 'log', '-5'], capture_output=True, encoding='utf-8', errors='replace')
# 3. Encontrá los lugares que dependen del default (receta de migración del PEP 686)
# python -X utf8=0 -X warn_default_encoding -m pytest
# 4. Solo como parche: sacar un proceso del nuevo default
# python -X utf8=0 agent.py (o PYTHONUTF8=0)Trampas de UTF-8 que muerden al código de agentes
Los archivos escritos antes con la code page ANSI de Windows y leídos con el nuevo default pueden lanzar UnicodeError o producir mojibake; el PEP 686 advierte también de corrupción silenciosa de datos. Python solo decide cómo decodificar un pipe — el proceso hijo decide qué bytes escribe, así que una herramienta que emite una code page heredada necesita decodificación explícita. En modo UTF-8 stdin y stdout usan el manejador de errores surrogateescape, y los archivos .pth ahora deben ser utf-8-sig.
sentinel y frozendict para código de tool calls y schemas
Los tool calls te entregan JSON donde una clave puede faltar o ser null explícito, y None no distingue ambos casos. sentinel te da un marcador único que sobrevive a copy y, para nombres a nivel de módulo, a pickle. frozendict es inmutable y, según el PEP 814, hasheable cuando todas sus claves y valores lo son, así que sirve como clave de dict o argumento de lru_cache. No es una subclase de dict, así que isinstance(x, dict) no lo reconoce, y desde 3.15 json.JSONEncoder lo codifica como objeto JSON.
MISSING = sentinel('MISSING') # built-in en 3.15; el repr es solo MISSING
def read_arg(args, key, default=MISSING):
value = args.get(key, MISSING)
if value is MISSING: # el modelo omitió la clave
if default is MISSING:
raise KeyError(f'tool call lacks required argument {key!r}')
return default
return value # None acá = null explícito del modelo
SEARCH_TOOL = frozendict(
name='web_search',
parameters=frozendict(type='object', required=('query',)),
)
CACHE_KEY = hash(SEARCH_TOOL) # funciona: toda clave y valor es hasheable
# SEARCH_TOOL['name'] = 'x' # TypeError: no admite asignación de items
payload = json.dumps(SEARCH_TOOL) # JSONEncoder acepta frozendict desde 3.15Tachyon: encontrá dónde pasa realmente el tiempo el loop del agente
El PEP 799 agrega un paquete profiling: profiling.tracing (determinístico, con cProfile como alias) y profiling.sampling, el profiler estadístico llamado Tachyon. Muestrea stacks desde fuera del proceso, y la documentación describe el overhead como virtualmente cero en el modo no bloqueante por defecto. Conectarse requiere privilegios (root o CAP_SYS_PTRACE en Linux, root o un entitlement de debugger en macOS, administrador o SeDebugPrivilege en Windows), y profiler y proceso objetivo deben usar la misma versión menor de Python. El viejo módulo profile está deprecado y se eliminará en 3.17.
# perfilar un script desde el arranque (por defecto: modo wall-clock, tabla de texto) python -m profiling.sampling run agent.py # conectarse a un worker vivo por PID durante 30 s y escribir un flame graph python -m profiling.sampling attach --flamegraph -d 30 -o profile.html 12345 # solo CPU, o seguir cadenas de await en código asyncio (corridas separadas: no se combinan) python -m profiling.sampling run --mode=cpu agent.py python -m profiling.sampling run --async-aware --flamegraph -o out.html agent.py # snapshot puntual del stack de un proceso colgado python -m profiling.sampling dump 12345
Regla de oro para 3.15
Sé explícito donde el default acaba de cambiar (encodings), sé lazy solo donde -X importtime demuestre la ganancia, y dejá que un profiler — no la intuición — elija qué optimizar en el loop del agente.
4. Ecosistema Listo y ¿Conviene Actualizar Ya?
Revisé la metadata de PyPI el 14 de octubre de 2026 para las librerías que más usan las apps LLM. Un classifier dice qué afirma probar un proyecto; los wheels dicen qué se instala sin compilador. No instalé nada en 3.15, así que tomalo como un mapa, no como un resultado de pruebas.
numpy
Lista 3.15 en sus classifiers. El release actual trae wheels cp315 y wheels free-threaded cp315t, incluidos win_amd64 y win_arm64.
pydantic y pydantic-core
pydantic lista 3.15 y es un wheel de Python puro. Su núcleo compilado, pydantic-core, publica wheels cp315 y cp315t incluido Windows, aunque su lista de classifiers todavía llega solo hasta 3.14.
SDKs de openai y anthropic
Ambos son wheels py3-none-any sin tope superior de Python, así que pip puede instalarlos en 3.15, pero sus classifiers llegan hasta 3.14. Su dependencia compilada jiter lista 3.15 y trae wheels cp315.
httpx y httpx2
El último release estable de httpx es 0.28.1 de diciembre de 2024, con classifiers hasta 3.12. Ambos SDKs ahora dependen de httpx2, la continuación del proyecto a cargo de Pydantic, que lista 3.15.
tiktoken y orjson
Ambos publican wheels cp315, incluido win_amd64.
Linters y type checkers
ruff, mypy y black listan 3.15. El PEP 810 dice que linters y formateadores necesitan actualizaciones para entender lazy, y un post de abril de 2026 en discuss.python.org dice que ruff ordena lazy imports desde 0.15.6. No pude confirmar cómo trata tu type checker la keyword: probalo primero.
Un classifier ausente no es una rotura
Los SDKs son wheels de Python puro, así que pip puede resolverlos en 3.15; sus classifiers simplemente no llegaron todavía. Es una señal para correr tu propia suite, no un motivo para entrar en pánico — ni para asumir que funciona.
✅ Actualizá, o abrí una rama, ya si
- • Enviás una CLI, hook o worker cuyo arranque está dominado por imports y controlás sus dependencias.
- • Mantenés una librería o framework: poné 3.15 en la matriz de CI ya, para que tus usuarios no sean los primeros en encontrar roturas.
- • Laptops o runners Windows siguen chocando con bugs de encoding: un default consistente ayuda, siempre que además agregues encodings explícitos.
- • Toda dependencia nativa de tu lockfile ya tiene wheel cp315.
⏳ Esperá unas semanas si
- • Una dependencia nativa de tu lockfile todavía no tiene wheel cp315; compilarla en CI o contenedores rara vez vale un cambio de runtime.
- • Usás APIs que 3.15 eliminó, como glob.glob0/glob1, importlib Loader.load_module, los módulos sre_* o la forma con keywords de NamedTuple: corré antes tu suite en 3.14 con warnings como errores.
- • Leés archivos heredados en code page de Windows con el open() por defecto y ningún test los cubre.
- • Tu CI fija un linter o type checker que no puede parsear lazy: igual podés actualizar el runtime y simplemente no usar la keyword.
5. Qué Hacer Esta Semana
Una semana enfocada le gana a una migración sin fecha. Esta es la secuencia que correría en cualquier codebase de agentes o LLM:
☀️ Días 1–2: Medir y encontrar los puntos de uso
- 1. Creá un venv descartable con 3.15 (en Windows, py install 3.15 usa el manifiesto de release de python.org) y corré tu suite de tests actual sin cambiar código.
- 2. Corré python -X importtime sobre tus entry points (--help y el subcomando más rápido) y listá los diez imports más caros.
- 3. Corré la suite una vez con python -X utf8=0 -X warn_default_encoding y buscá con grep open(, read_text, write_text y llamadas de subprocess en modo texto sin encoding=.
📅 Días 3–4: Corregir y adoptar
- Encodings: agregá encoding='utf-8' (o 'locale' a propósito) en cada punto señalado y decodificá explícitamente la salida de subprocess donde la herramienta no sea UTF-8.
- Lazy imports: marcá como lazy a nivel de módulo los tres a cinco imports más pesados y poco usados, y volvé a correr importtime para confirmar que salieron del camino de arranque.
- Código de tool calls: reemplazá object() y strings mágicos para argumentos faltantes por sentinel, y congelá schemas de tools y configs por defecto con frozendict.
📆 Día 5: Perfilar y decidir
- Perfilar: conectá Tachyon a una corrida real de un agente, primero modo wall y después --async-aware, y anotá los tres frames principales.
- Desplegar: actualizá primero CLIs y workers y al final los servicios de API de larga vida; mantené 3.14 y 3.15 en la matriz de CI unas semanas.
- Compatibilidad: si un solo árbol de fuentes debe servir a ambas versiones, usá __lazy_modules__ en vez de la keyword lazy.
Conclusión
Python 3.15 no es una reescritura. Es un conjunto de cambios bien acotados que calzan con el dolor de las apps LLM: arranques en frío lentos, sorpresas de encoding en Windows, argumentos faltantes versus null ambiguos y loops de agente que nadie puede perfilar. Dos son opt-in, uno no lo es, y el JIT y abi3t quedan de fondo por ahora.
Corré el plan de una semana de arriba y vas a saber, con tus propios números y no los míos ni los del PEP, si los lazy imports se ganan su lugar y si tus rutas en Windows sobreviven al nuevo default.
Esta semana en una pantalla
Medir
- • importtime en entry points
- • Tachyon en una corrida real
- • wheels cp315 en el lockfile
Hacer explícito
- • encoding= en E/S de archivos
- • Decodificación explícita en pipes
- • sentinel para args faltantes
Esperar
- • JIT: todavía experimental
- • abi3t: autores de extensiones
- • Modo lazy global: filtro primero
Fuentes
- Python.org — Lanzamiento de Python 3.15.0
- Documentación de Python — Novedades de Python 3.15
- PEP 810 — Imports lazy explícitos
- Documentación de Python — Referencia del lenguaje: lazy imports
- PEP 686 — Activar el modo UTF-8 por defecto
- Documentación de Python — Usar Python en Windows: modo UTF-8
- PEP 661 — Valores centinela
- PEP 814 — Tipo built-in frozendict
- Documentación de Python — profiling.sampling (Tachyon)
- PEP 790 — Calendario de lanzamiento de Python 3.15
- PEP 803 — ABI estable abi3t para builds free-threaded
- PyPI — pydantic-core (lista de wheels)
- PyPI — httpx2



