Python 3.15 ya está disponible: qué se rompe al actualizar y los dos cambios que debes probar primero

Python 3.15.0 es la última versión estable de Python: salió el 9 de octubre de 2026 y, antes de actualizar, hay dos cambios que debes probar. UTF-8 pasa a ser la codificación por defecto en todas las plataformas y la nueva palabra clave lazy import cambia cuándo se cargan tus módulos. La versión también trae un frozendict integrado, un tipo sentinel y un profiler por muestreo llamado Tachyon, y elimina APIs de diecisiete módulos de la biblioteca estándar.

¿Qué hay de nuevo en Python 3.15?

La versión reúne 5.643 commits de 1.012 personas. Estos son los cambios que afectan al código del día a día:

  • PEP 810, lazy imports explícitos. Una nueva soft keyword, lazy, retrasa la carga de un módulo hasta el primer uso del nombre.
  • PEP 686, UTF-8 por defecto. Toda la E/S sin argumento encoding usa UTF-8, sin importar el locale del sistema.
  • PEP 814, frozendict. Un mapping integrado, inmutable y hashable.
  • PEP 661, sentinel. Un tipo integrado para crear valores centinela únicos.
  • PEP 798, desempaquetado en comprensiones. [*L for L in lists] ahora aplana listas.
  • PEP 799, un paquete profiling. Incluye Tachyon, un nuevo profiler estadístico por muestreo.
  • Mejores mensajes de error. Python reconoce métodos de otros lenguajes: [1, 2, 3].push(4) sugiere .append y 'hello'.toUpperCase() sugiere .upper.

También hay mejoras internas:

  • El JIT experimental recibió una actualización importante.
  • Los binarios oficiales de Windows de 64 bits usan ahora el intérprete tail-calling.
  • El instalador de macOS incluye soporte free-threading por defecto.

¿Cómo instalar Python 3.15?

En Windows, usa el Python install manager:

py install 3.15

En macOS, descarga el instalador desde la página de la versión. Funciona en macOS 10.15 y posteriores.

Para el resto de plataformas están los tarballs del código fuente.

Un aviso para macOS, vigente al momento de publicar esta nota: la página de la versión advierte que IDLE y otras aplicaciones con tkinter pueden colgarse en macOS 27.0 cuando un menú abre un diálogo. La causa es un cambio del sistema operativo y afecta a todas las versiones actuales de Python. Al 9 de octubre de 2026 no había solución; el seguimiento está en cpython#158053.

Si usas uv, Barry Warsaw publicó una aventura de texto que recorre las novedades de 3.15. Cada acertijo se resuelve escribiendo código real de 3.15:

uvx --python 3.15 whatsnewt

¿Qué debes probar primero al actualizar Python?

Prueba primero el cambio de codificación, porque puede corromper datos sin dar error. Después prueba los lazy imports, porque son el cambio que de verdad vas a querer adoptar.

¿Python 3.15 usa UTF-8 por defecto?

Sí. Toda llamada que lee o escribe texto sin argumento encoding usa ahora UTF-8, sin importar el locale del sistema. Por ejemplo:

open('data.txt')

El PEP 686 es explícito sobre a quién afecta. La mayoría de los sistemas Unix ya usan un locale UTF-8, así que el cambio afecta sobre todo a quien trabaja en Windows. Si tu código dependía de la codificación por defecto del locale, puedes ver UnicodeError, mojibake o corrupción silenciosa de datos. El cambio cubre archivos, stdio y pipes, así que las llamadas a subprocess con text=True también se ven afectadas.

El propio PEP propone una migración en tres pasos.

1. Encuentra cada punto donde tu código depende del valor por defecto. Activa la advertencia opcional:

python -X warn_default_encoding tu_app.py

o define la variable de entorno:

PYTHONWARNDEFAULTENCODING=1

Python emitirá un EncodingWarning en cada lugar donde se use la codificación por defecto.

2. Haz cada llamada explícita.

  • Usa encoding="utf-8" si los datos son UTF-8.
  • Usa encoding="locale" si de verdad quieres la codificación del sistema. Está soportado desde Python 3.10.
  • Si llamas a locale.getpreferredencoding(), cámbialo por "utf-8" o por locale.getencoding().

3. Gana tiempo si lo necesitas. Puedes desactivar el nuevo comportamiento con PYTHONUTF8=0 o -X utf8=0. Úsalo como puente, no como solución.

¿Cómo funcionan los lazy imports en Python 3.15?

Pon lazy delante del import. El módulo se carga la primera vez que usas el nombre, no al arrancar:

lazy import json
lazy from pathlib import Path

print("Arrancando...")  # json y pathlib todavía no se importaron del todo

data = json.loads('{"key": "value"}')  # aquí se importa json
p = Path(".")  # aquí se importa pathlib

La ganancia está en el tiempo de arranque de aplicaciones que importan muchos módulos pero solo usan algunos en cada ejecución: CLIs, test runners y funciones serverless. Mantienes todos los imports arriba del archivo, sin repartir imports dentro de funciones para esquivar el costo.

Antes de adoptarlo, ten en cuenta cuatro cosas:

  • Los errores cambian de lugar. Si un módulo importado con lazy no existe, la excepción salta en el primer uso, no en el import. El traceback muestra tanto la línea donde se usó el nombre como la línea original del import.
  • Dónde se permite. lazy solo funciona a nivel de módulo. Dentro de una función, del cuerpo de una clase o de un bloque try/except/finally es un SyntaxError. También lo son lazy from module import * y lazy from __future__ import ....
  • Activarlo de forma global. -X lazy_imports=all o PYTHON_LAZY_IMPORTS=all hacen lazy todos los imports sin tocar el código. normal, el valor por defecto, solo respeta la palabra clave.
  • Compatibilidad con versiones anteriores. Si tu código también debe correr en versiones anteriores a 3.15, define __lazy_modules__. Los import normales de los módulos listados se vuelven lazy en 3.15 y siguen siendo normales en versiones anteriores:
__lazy_modules__ = ["json", "pathlib"]

import json     # lazy
import os       # sigue siendo eager

Para un control más fino, sys.set_lazy_imports_filter() acepta una función. La documentación la usa para hacer lazy solo los módulos de tu propia aplicación y mantener eager las dependencias de terceros:

import sys

def myapp_filter(importing, imported, fromlist):
    return imported.startswith("myapp.")
sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")

¿Qué se rompe al actualizar a Python 3.15?

La mayoría de lo eliminado son APIs marcadas como obsoletas desde 3.12 o 3.13. Si tu suite de tests corre con las advertencias de deprecación activas, probablemente ya las viste. Estas son las que tienen más probabilidad de romper código real:

Módulo Qué cambió
datetime strptime() lanza ValueError si el formato tiene %d pero no año
http.server Se eliminan CGIHTTPRequestHandler y la opción --cgi
sre_compile, sre_constants, sre_parse Se eliminan los módulos
typing Ya no se admite la sintaxis con keywords NamedTuple("Point", x=int, y=int) ni TypedDict("TD") sin campos
typing Se elimina no_type_check_decorator
ast Los constructores de nodos lanzan TypeError si falta un argumento obligatorio o recibe un keyword desconocido
pathlib Se elimina PurePath.is_reserved() (usa os.path.isreserved())
glob Se eliminan glob0() y glob1() (usa glob.glob() con root_dir)
zipimport Se elimina zipimporter.load_module() (usa exec_module())
importlib.resources Se elimina el parámetro package de files()
platform Se elimina java_ver()
types Se elimina codeobject.co_lnotab
wave Se eliminan getmark(), setmark() y getmarkers()

La sección de migración de la documentación también lista cambios de comportamiento que no son eliminaciones:

  • sqlite3: todos los parámetros de sqlite3.connect() salvo database pasan a ser keyword-only.
  • argparse: en add_argument('-f', '-foo'), dest ahora es 'foo' y no 'f'. Pasa dest de forma explícita para mantener el comportamiento anterior.
  • unittest: assertWarns() y assertWarnsRegex() ya no se tragan las advertencias que no coinciden. Tus tests pueden empezar a mostrar advertencias que antes quedaban ocultas.
  • base64: urlsafe_b64decode() ya no exige padding. Pasa padded=True si tu código dependía de ello.

¿Qué es frozendict y en qué se diferencia de dict?

frozendict es un nuevo mapping integrado e inmutable: una vez creado, no se puede modificar. Es hashable siempre que sus claves y valores lo sean, así que puede ser clave de un dict o elemento de un set, y sirve donde antes improvisabas una clave de caché.

>>> a = frozendict(x=1, y=2)
>>> a['z'] = 3
TypeError: 'frozendict' object does not support item assignment
>>> b = frozendict(y=2, x=1)
>>> a == b
True

El detalle clave es que frozendict no es una subclase de dict: hereda directamente de object. Por eso cualquier comprobación isinstance(arg, dict) lo rechaza. La documentación sugiere dos reemplazos:

  • isinstance(arg, (dict, frozendict))
  • isinstance(arg, collections.abc.Mapping), que además acepta tipos como MappingProxyType

frozendict conserva el orden de inserción, pero las comparaciones lo ignoran. La biblioteca estándar ya lo acepta en copy, json, pickle, pprint y otros módulos. json.loads suma además un parámetro array_hook: si pasas frozendict como object_pairs_hook y tuple como array_hook, obtienes desde JSON una estructura totalmente inmutable.

¿Cómo usar Tachyon, el nuevo profiler de Python?

Tachyon vive en profiling.sampling. Toma instantáneas periódicas del call stack desde fuera del proceso, por lo que el equipo de CPython describe su sobrecarga sobre el proceso perfilado como prácticamente nula. Su función más interesante es que puede conectarse a un proceso que ya está corriendo, lo que lo hace utilizable en producción.

python -m profiling.sampling run script.py
python -m profiling.sampling run --flamegraph -o profile.html script.py
python -m profiling.sampling attach 12345
python -m profiling.sampling dump 12345

  • dump imprime una sola instantánea con formato de traceback. Es útil con un proceso colgado.
  • --live abre en la terminal una vista al estilo top.
  • --mode cpu mide solo tiempo de CPU, y --mode gil mide el tiempo que se retiene el GIL.

Conectarse a un proceso requiere permisos:

  • Linux: root, la capability CAP_SYS_PTRACE o un ptrace scope más permisivo.
  • macOS: root o un entitlement de depurador.
  • Windows: privilegios de administrador.

El profiler y el proceso deben usar la misma versión menor, así que no puedes conectarte desde 3.14 a un proceso en 3.15.

El viejo cProfile se mantiene como alias. profile queda obsoleto y se eliminará en 3.17.

¿Qué más cambia: sentinel, comprensiones y el JIT?

  • sentinel es un nuevo tipo integrado para crear valores marcadores únicos. Conservan su identidad al copiarse, funcionan en expresiones de tipo con | y se pueden serializar con pickle si se pueden importar por módulo y nombre.
  • El desempaquetado en comprensiones reemplaza las comprensiones anidadas e itertools.chain(): [*L for L in lists] aplana una lista de listas y {**d for d in dicts} fusiona diccionarios.
  • El JIT sigue siendo experimental. Según las mediciones de pyperformance del propio equipo de CPython, publicadas en la página de la versión, mejora la media geométrica entre 7 y 8 por ciento frente al intérprete estándar en Linux x86-64, y entre 11 y 12 por ciento en macOS AArch64 frente al intérprete tail-calling. Son benchmarks autoinformados, no una garantía para tu carga de trabajo.
  • El garbage collector vuelve a ser el generacional. Python 3.14.0 a 3.14.4 incluyó un recolector incremental. Tras reportes de presión de memoria importante en producción, se revirtió desde 3.14.5, y 3.15 mantiene el generacional.

¿Python 3.15 o Python 3.14: conviene actualizar ya?

Depende de lo que toque tu código:

  • Si trabajas sobre todo en Unix y ya pasas encoding de forma explícita: la actualización debería ser de bajo riesgo. Las ganancias principales son los lazy imports para el arranque y Tachyon para perfilar.
  • Si estás en Windows o lees archivos sin una codificación explícita: corre primero con -X warn_default_encoding y corrige cada advertencia antes de cambiar.
  • Si dependes de APIs eliminadas: la tabla de arriba te dice qué buscar en tu código.
  • Si mantienes librerías o dependes de muchas: espera a que tus dependencias críticas publiquen wheels para 3.15, que suele ser el cuello de botella.

¿Y tú? ¿Qué versión de Python corre hoy en tu producción?

¿Vas a migrar a Python 3.15 este trimestre?
  • Sí, ya lo estoy probando
  • Cuando mis dependencias lo soporten
  • Este año no
  • Prefiero otra opción (cuéntanos cuál)
0 votantes

Comenta abajo o, si este artículo te llegó por correo, responde directamente al email: tu respuesta se publica aquí.