Volver al blog
Código abierto

Python 3.15 salió el 9 de octubre. En Windows, los archivos abiertos sin codificación ahora usan UTF-8 por defecto

19 de octubre de 2026 6 min de lectura

El viernes 9 de octubre salió Python 3.15.0. La página de la versión cuenta 5.643 commits de 1.012 contribuidores. Ocho días antes, el 1 de octubre, Python 3.10 llegó al final de su vida útil: 3.10.22 fue su última versión de seguridad, y la rama está ahora congelada.

Así que, para quien siga en 3.10, la pregunta ya no es si migrar. Es adónde aterrizar y qué se rompe por el camino.

Buena parte del pegamento alrededor de mi trabajo es Python: los scripts que publican estos artículos, la pasada de verificación de datos en la que Jev juzga mis borradores, unas cuantas tareas programadas. Así que, para 3.15, leí el final de la página What's New antes del recorrido por las novedades: las secciones Removed y Porting. Ahí vive el coste de la migración.

El valor por defecto que cambió bajo cada script

La línea más importante no está en la sección Porting. Está bajo «Other language changes»: Python ahora usa UTF-8 como codificación por defecto, «independientemente del entorno del sistema». Un open() en modo texto sin argumento encoding ahora lee y escribe en UTF-8.

En Linux y macOS, eso casi no cambia nada. La PEP 686, que tomó la decisión, lo dice sin rodeos: «este cambio afecta sobre todo a los usuarios de Windows». La documentación de Windows explica por qué. Windows «todavía usa codificaciones heredadas para la codificación del sistema (la página de códigos ANSI)», y hasta 3.15 eso era lo que Python usaba para los archivos de texto cuando no se indicaba codificación.

La PEP es igual de clara sobre el riesgo: cuando un programa depende de la codificación por defecto, el cambio «puede provocar UnicodeError, mojibake o incluso corrupción silenciosa de datos».

Imagina un script en un PC con Windows de una pequeña empresa, que lee un CSV escrito por un software antiguo en la página de códigos del sistema, con nombres acentuados. En 3.14 lo lee sin problema. En 3.15, el mismo open() lanza una UnicodeDecodeError en la primera «é», y todo archivo que Python reescribe ahora está en UTF-8, que la herramienta antigua puede leer como galimatías. Nadie tocó el código. La PEP también cita las tuberías de subprocesos en modo texto (text=True) entre los casos que corregir.

Las soluciones están todas documentadas:

  • Pasa encoding="utf-8" donde quieras UTF-8, y encoding="locale" (compatible desde 3.10) donde de verdad quieras la codificación de la máquina. En Windows, el códec «mbcs» sigue dándote la página de códigos ANSI.
  • Para encontrar cada llamada que depende del valor por defecto, ejecuta tus tests con -X warn_default_encoding o con la variable de entorno PYTHONWARNDEFAULTENCODING. Esa advertencia existe desde 3.10, así que puedes activarla en tu versión actual, antes de migrar.
  • Como vía de escape, PYTHONUTF8=0 o -X utf8=0 restaura el comportamiento antiguo. Lo trataría como un ticket pendiente, no como una solución.

Un formato de fecha que ahora lanza error

En la sección Removed: datetime.strptime() ahora lanza ValueError cuando el formato contiene %d sin año. Estaba obsoleto desde 3.13.

La razón es un fallo que solo aparece en los días bisiestos. Sin año en el formato, strptime rellena uno por defecto, 1900, que no es bisiesto. Analiza «29/02» con "%d/%m" y falla, mientras que todos los demás días del año pasan. La solución documentada es incluir siempre un año y, si tus cadenas no lo tienen, añadir tú mismo un año bisiesto antes de analizar.

Si extraes fechas como «14/07» de documentos, correos o nombres de archivo, busca con grep en tus formatos los %d sin %Y ni %y. Ese código solo fallaba en un día bisiesto. En 3.15 falla en cada llamada, lo cual es mejor, siempre que lo encuentres antes que producción. Busca también %e: sin año, ahora emite un DeprecationWarning, con eliminación prevista para 3.17.

El resto de la lista que pasaría por grep

Las secciones Porting, Removed y las notas de módulos son largas, pero un puñado de líneas toca código de aplicación corriente:

  • sqlite3.connect() ahora solo acepta la ruta de la base de datos como argumento posicional. sqlite3.connect("app.db", 10) deja de funcionar; escribe timeout=10.
  • En argparse, add_argument('-f', '-foo') ahora guarda el valor bajo foo, no bajo f. Pasa un dest explícito para conservar el nombre antiguo.
  • base64.urlsafe_b64decode() ya no exige relleno (padding). Si tu validación contaba con que rechazara entradas sin relleno, pasa padded=True.
  • assertWarns() de unittest ya no se traga las advertencias que no coinciden. Una suite que trata las advertencias como errores puede ponerse en rojo sin cambios de código.
  • importlib.metadata.metadata() lanza MetadataNotFound cuando falta el archivo de metadatos de una distribución, donde antes devolvía un objeto vacío.

Luego las eliminaciones, que muerden sobre todo por dependencias antiguas más que por tu propio código: los módulos sre_compile, sre_constants y sre_parse, el método load_module() de los cargadores de importación (obsoleto desde 3.4), CGIHTTPRequestHandler y la opción --cgi de http.server, la sintaxis de argumentos con nombre de NamedTuple. Tu archivo de bloqueo (lockfile) es lo primero que miraría.

Lo que justifica el cambio

Los imports perezosos son el titular, y con razón. Escribe lazy import json y el módulo se carga la primera vez que se usa el nombre, no al arrancar. La PEP 810 dice que esto «puede reducir el tiempo de arranque entre un 50 y un 70 % en la práctica» en herramientas de línea de comandos, y que ahorros de memoria del 30 al 40 por ciento «se han observado en cargas de trabajo reales». Son las cifras de la PEP, no mías; -X importtime muestra adónde va tu propio tiempo de arranque.

Tres salvedades de la especificación. lazy solo funciona a nivel de módulo: dentro de una función, el cuerpo de una clase o un bloque try/except, es un SyntaxError, así que el recurso clásico de la dependencia opcional sigue siendo inmediato. Un import perezoso que falla ahora falla en el primer uso, quizá en lo más hondo de una petición, en lugar de al arrancar. Y los módulos que se registran al importarse, el patrón del registro de plugins, no se registrarán hasta que algo los toque.

Dos mejoras más discretas. Los binarios oficiales de Windows de 64 bits ahora usan el intérprete de llamadas terminales: la página What's New informa de una aceleración del 15 al 20 por ciento en la media geométrica de pyperformance en Windows x86-64 frente al intérprete de switch-case, medida en un AMD Ryzen 7 5800X, y de mejoras observadas del 14 al 40 por ciento según la carga. No requiere cambios de código. Y Tachyon, el nuevo perfilador de muestreo de profiling.sampling, se engancha a un proceso en ejecución por su PID, con un modo dump que imprime la pila de cada hilo. Para una tarea que se cuelga en un servidor en plena noche, es la función que buscaría primero.

El JIT está mejorado pero sigue siendo experimental: del 7 al 8 por ciento en la media geométrica en Linux x86-64, con benchmarks individuales que van desde un 15 por ciento más lentos hasta más del doble de rápidos. No es motivo para migrar por sí solo.

La lección de 3.14

Una línea de la sección gc merece más atención de la que recibirá. Python 3.14.0 a 3.14.4 incluyeron un nuevo recolector de basura incremental. Tras «varios informes de una presión de memoria significativa en entornos de producción», se revirtió al recolector de 3.13, que es el que usan 3.14.5 y posteriores, y 3.15.

Cinco versiones de una rama estable funcionaron con un recolector que luego se retiró. Eso no es motivo para evitar 3.15. Es motivo para ponerla en un solo canario con tráfico real y vigilar la memoria antes de desplegarla en todas partes.

Qué haría este mes

Si algo sigue en 3.10, muévelo ya, a 3.14 o 3.15. No recibe más correcciones de seguridad.

Antes de tocar 3.15, ejecuta la suite de tests en tu versión actual con -X warn_default_encoding y corrige cada advertencia con una codificación explícita. Pasa tus formatos strptime por grep. Contrasta la lista de portabilidad de arriba con tu código y tu lockfile.

Después, un servicio, un canario, tráfico real, una semana de gráficas de memoria. La primera versión de corrección, 3.15.1, está prevista para el 1 de diciembre. A partir de ahí, 3.15 recibe versiones de corrección durante unos dos años y correcciones de seguridad solo en código fuente hasta octubre de 2031 aproximadamente, así que no hay prisa por ser el primero, solo por salir de 3.10.

La palabra clave de la que todo el mundo hablará es lazy. El cambio con más probabilidades de romper un script que funciona es el que nadie escribió: un valor por defecto.

Fuentes

¿Un proyecto del mismo estilo?

Diseño y despliego productos como este. Hablemos.

Hablemos