Python 3.15 ya está aquí: importaciones perezosas, frozendict y UTF-8 por defecto

La Python Software Foundation publicó el 9 de octubre Python 3.15.0, la nueva versión estable del lenguaje. Trae dos tipos integrados nuevos, una palabra clave para importar módulos solo cuando se usan y un cambio que acaba con muchos problemas de acentos: UTF-8 pasa a ser la codificación por defecto. Compilamos la versión final desde el código fuente oficial y probamos cada novedad.

Python publica una versión nueva cada octubre, con un ciclo de soporte de cinco años: dos de correcciones y tres más solo de seguridad. La 3.14 salió hace un año; la 3.15 es la primera versión estable que recoge el trabajo de estos doce meses, y es la que conviene empezar a probar con tus proyectos.

Las novedades se reparten entre el lenguaje (importaciones perezosas, frozendict, sentinel, desempaquetado en comprensiones), el comportamiento por defecto (UTF-8 en todas partes), las herramientas (un perfilador por muestreo en la biblioteca estándar) y el rendimiento, con un compilador JIT experimental mejorado.

Claves de Python 3.15

Importaciones perezosas con lazy import

import sys, time

inicio = time.perf_counter()
lazy import json
lazy import xml.dom.minidom
print("tras las importaciones:", "json" in sys.modules, "xml.dom.minidom" in sys.modules)
print(f"tiempo: {(time.perf_counter() - inicio) * 1000:.2f} ms")

print(json.dumps({"listo": True}))
print("después de usar json:", "json" in sys.modules, "xml.dom.minidom" in sys.modules)
python3.15 perezoso.pytras las importaciones: False False
tiempo: 0.01 ms
{"listo": true}
después de usar json: True False

Con lazy import, el módulo no se carga al llegar a esa línea sino la primera vez que se usa. Por eso, tras las dos importaciones, ninguno está en sys.modules; json se carga al llamar a json.dumps, y xml.dom.minidom, que no se llega a usar, no se carga nunca. En programas con muchas dependencias, sobre todo herramientas de línea de órdenes, esto acorta el arranque. También se puede activar para todo un programa con python -X lazy_imports=all o la variable PYTHON_LAZY_IMPORTS. Es una palabra clave nueva: Python 3.14 no la entiende.

el mismo código en Python 3.14.8  File "//v.py", line 1
    lazy import json
         ^^^^^^
SyntaxError: invalid syntax

frozendict: diccionarios inmutables

config = frozendict(host="localhost", puerto=5432)
print(config, config["puerto"])

try:
    config["puerto"] = 3306
except TypeError as e:
    print("TypeError:", e)

a = frozendict(x=1, y=2)
b = frozendict(y=2, x=1)
print(a == b, hash(a) == hash(b))

cache = {frozendict(lenguaje="python", version="3.15"): "lista"}
print(cache[frozendict(version="3.15", lenguaje="python")])
print(isinstance(config, dict))
salidafrozendict({'host': 'localhost', 'puerto': 5432}) 5432
TypeError: 'frozendict' object does not support item assignment
True True
lista
False

Es el equivalente de frozenset para los diccionarios: no se puede modificar y, si sus claves y valores lo son, se puede usar como clave de otro diccionario o meter en un conjunto. Conserva el orden de inserción, pero dos frozendict con el mismo contenido son iguales aunque estén en distinto orden. No hereda de dict (de ahí el False final), así que para aceptar ambos conviene comprobar collections.abc.Mapping.

sentinel: marcadores únicos sin trucos

FALTA = sentinel("FALTA")
print(FALTA, type(FALTA).__name__)

def buscar(datos, clave, por_defecto=FALTA):
    if clave in datos:
        return datos[clave]
    if por_defecto is FALTA:
        raise KeyError(clave)
    return por_defecto

datos = {"a": None}
print(buscar(datos, "a"))
print(buscar(datos, "b", 0))
try:
    buscar(datos, "b")
except KeyError as e:
    print("KeyError:", e)

import copy
print(copy.deepcopy(FALTA) is FALTA)
salidaFALTA sentinel
None
0
KeyError: 'b'
True

Sirve para distinguir «no me han pasado nada» de «me han pasado None», algo que hasta ahora se hacía con un object() anónimo. El nuevo tipo tiene un nombre legible, sobrevive a una copia sin dejar de ser el mismo objeto y se puede usar en las anotaciones de tipo (FALTA | None).

Desempaquetado dentro de las comprensiones

listas = [[1, 2], [3, 4], [5]]
print([*l for l in listas])

conjuntos = [{1, 2}, {2, 3}, {3, 4}]
print({*s for s in conjuntos})

ajustes = [{"tema": "claro"}, {"idioma": "es"}, {"tema": "oscuro"}]
print({**d for d in ajustes})
salida[1, 2, 3, 4, 5]
{1, 2, 3, 4}
{'tema': 'oscuro', 'idioma': 'es'}

El * y el ** ahora funcionan dentro de una comprensión. [*l for l in listas] aplana una lista de listas sin el doble for de antes, y {**d for d in ajustes} une diccionarios: si una clave se repite, gana el último.

UTF-8 por defecto

import sys, locale
with open("saludo.txt", "w") as f:
    f.write("Año, niño, café ☕")
with open("saludo.txt", "rb") as f:
    print(f.read())
print("modo UTF-8:", sys.flags.utf8_mode, "| locale:", locale.getpreferredencoding(False))
LANG=C python3.15 utf8.pyb'A\xc3\xb1o, ni\xc3\xb1o, caf\xc3\xa9 \xe2\x98\x95'
modo UTF-8: 1 | locale: utf-8

Aunque el sistema esté configurado con la codificación más básica (LANG=C), open() escribe en UTF-8 sin que se lo pidas. Hasta ahora usaba la del sistema, y por eso un programa que funcionaba en Linux fallaba con los acentos en Windows. Si algún programa antiguo depende del comportamiento anterior, se puede volver a él con PYTHONUTF8=0; así se ve lo que pasaba antes:

LANG=C PYTHONUTF8=0 python3.15 utf8.pyUnicodeEncodeError: 'ascii' codec can't encode character '\xf1' in position 1: ordinal not in range(128)

Errores que sugieren el método correcto

python3.15 -c …, cuatro errores típicos de quien viene de otro lenguajeAttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?
AttributeError: 'dict' object has no attribute 'put'. Use d[k] = v.
AttributeError: 'tuple' object has no attribute 'append'. Did you mean to use a 'list' object?

Cuando alguien escribe un método de JavaScript o de Java, Python 3.15 sugiere el suyo: push → append, toUpperCase → upper. Las sugerencias aparecen en el mensaje que se imprime cuando el error no se captura; el texto de la excepción, si lo capturas con except, sigue siendo solo la primera parte. En Python 3.14 los mismos errores no sugieren nada.

Un perfilador nuevo: Tachyon

python3.15 -m profiling.sampling run lento.py
salida (recortada)Captured 262 samples in 0.26 seconds
Sample rate: 1000.00 samples/sec
       nsamples   sample%  tottime (ms)    cumul%  cumtime (ms)  filename:lineno(function)
        156/216      67.0       156.000      92.7       216.000  lento.py:7(ordenar)
          60/60      25.8        60.000      25.8        60.000  lento.py:7(ordenar.<locals>.<genexpr>)
          16/17       6.9        16.000       7.3        17.000  lento.py:2(fib)

El nuevo paquete profiling reúne los perfiladores de la biblioteca estándar. profiling.sampling, al que llaman Tachyon, mira cada poco qué está ejecutando el programa en lugar de medir cada llamada, así que casi no lo frena y puede incluso engancharse a un proceso que ya está en marcha (attach). Aquí señala que el 67 % del tiempo se va en la función ordenar. También genera gráficos de llama en HTML. cProfile sigue funcionando: ahora es un alias de profiling.tracing.

Lo demás

QuéEn resumen
Compilador JITSigue siendo experimental y no viene activado por defecto. Según el anuncio, rinde entre un 7 y un 8 % más (media geométrica) en x86-64 con Linux. No lo hemos medido.
TipadoTypedDict con claves extra tipadas (PEP 728), TypeForm (PEP 747) y bases disjuntas (PEP 800)
CompilaciónPunteros de marco activados por defecto (PEP 831), que facilitan perfilar desde fuera; en Windows, el intérprete con llamadas de cola; en macOS, la versión sin GIL se instala por defecto
RetiradoLos módulos sre_* (hay que usar re), el atributo __cached__ de los módulos y funciones obsoletas de ast, pathlib, typing y otros módulos

Por qué importa

La mayoría de estos cambios ahorran trabajo repetido. Las importaciones perezosas se hacían hasta ahora con importaciones escondidas dentro de las funciones; los diccionarios inmutables, con types.MappingProxyType o paquetes externos; los centinelas, con un object() sin nombre que aparecía en los mensajes de error como <object object at 0x7f…>. Ahora todo eso viene con el lenguaje y con una forma única de hacerlo.

El cambio a UTF-8 es el que más se notará sin hacer nada. Es la causa de una parte enorme de los errores con acentos y eñes en Windows, y para quien programa en español era un tropiezo habitual. Eso sí: un programa que dependía de la codificación del sistema puede comportarse de otra forma. Lo más prudente sigue siendo indicar siempre encoding="utf-8" al abrir archivos.

Python 3.15 se puede descargar ya desde python.org. Antes de pasar un proyecto a producción conviene esperar a que tus dependencias publiquen versiones para la 3.15, algo que suele llevar unas semanas, y probarlo primero en un entorno virtual. Para empezar con el lenguaje, nuestro curso de Python y los ejemplos de Python siguen sirviendo tal cual.

Probado compilando el código fuente oficial de Python 3.15.0 (Python-3.15.0.tar.xz) en un contenedor Debian; las salidas están copiadas de esa ejecución, y las de comparación, de Python 3.14.8. Detalle de cada cambio en What’s New in Python 3.15.

Fuente: Python Software Foundation, «Python 3.15.0»