Categoría: Python

¡Faltan 13 días para Python 3.15! Revisa si tu código se romperá - UTF-8 por defecto (adiós a cp949) y checklist de migración para lazy import 🐍

Esta entrada es una traducción del original en coreano.

Índice12 secciones

¡Hola! Soy Mojangcam 😎

Por fin, la versión final de Python 3.15 sale el 1 de octubre de 2026. Hoy es 18 de septiembre, así que estamos justo en D-13: faltan 13 días.

Hasta ahora en este blog he cubierto 3.15 en los posts de la beta 4, la rc1 y la rc2, centrándome sobre todo en "qué hay de nuevo". Hoy vamos a cambiar un poco el enfoque. En lugar de pasear por las novedades, este post trata de comprobar "¿El código que uso ahora mismo se romperá en 3.15?"

Esta versión 3.15 es un cambio muy grande, sobre todo para quienes usan Python en Corea. El famoso infierno de la codificación cp949 por fin se termina. Pero, a cambio, el código que hasta ahora dependía de cp949 puede romperse justo en la dirección contraria. 😅

📌 Lo que necesitas

  • Python 3.15.0rc2 (publicada el 2026-09-01, la última rc antes de la final)
  • Tu Python 3.13 o 3.14 de siempre (conviene tenerlo instalado en paralelo para comparar)
  • Algunos archivos de texto para probar (a ser posible, un archivo en coreano guardado en cp949)
  • Tu editor favorito (recomiendo VS Code)

El objetivo es probar antes de la versión final, así que no desinstales tu versión actual: instala 3.15 aparte. En Windows, el lanzador py se instala junto con Python, así que puedes elegir la versión con py -3.15 script.py.

Terminal
# Ver las versiones de Python instaladas (Windows)
py --list

# Ejecutar con 3.15
py -3.15 test.py

# macOS/Linux
python3.15 test.py

🧠 ¿Por qué revisarlo ahora?

Mucha gente piensa: "Ya me ocuparé cuando salga la final". Pero esta vez es un poco distinto.

La PEP 686 de 3.15 cambia el valor por defecto de la propia forma en que se abren los archivos. Lo que da miedo es que las cosas pueden romperse en silencio, sin ningún error. El programa funciona bien, pero el texto en coreano de tu CSV aparece como ?????, o abres un archivo de log y está lleno de caracteres ilegibles.

Esto pasa sobre todo con los scripts que se ejecutan automáticamente cada día (automatización de Excel, crawling, envío de correos, etc.). Como nadie revisa el resultado cada vez, lo descubres días después. Por eso hace falta revisarlo con antelación.

🧩 Cambio 1 - Ahora los archivos siempre se abren en UTF-8 (PEP 686)

Empecemos por el cambio más importante.

Hasta 3.14, si no indicabas encoding en open(), se usaba la codificación del locale del sistema operativo. En un Windows en coreano, esa era cp949 (de la familia euc-kr). En macOS y Linux, era UTF-8. De ahí venía el famoso problema: el mismo código se rompía en Windows y funcionaba perfectamente en un Mac.

A partir de 3.15, es siempre UTF-8, sin importar el sistema operativo.

Python
# Python 3.15 / ¿Y si no indicas la codificación?

with open("메모.txt") as f:      # 3.14 o anterior (Windows) → cp949
    print(f.read())               # 3.15 → UTF-8 (sin importar el SO)

Vamos a desglosarlo 💡

  • Codificación por defecto de open(): fija en UTF-8 desde 3.15. Y eso aunque no toques variables de entorno como PYTHONUTF8.
  • subprocess(text=True): también funciona con UTF-8 como base.
  • encoding="locale": si quieres indicar de forma explícita "sigue el locale del sistema operativo" como antes, usa este valor especial.
  • locale.getencoding(): una función para averiguar cuál es realmente la codificación del locale actual del sistema operativo.

🛠️ Cómo saber en 30 segundos si tu código está en riesgo

La forma más rápida es encontrar todas las llamadas a open() que omiten la codificación. Python lleva tiempo teniendo una opción de advertencia justo para esto.

Terminal
# Muestra advertencias en el código que depende de la codificación por defecto
py -3.15 -X warn_default_encoding my_script.py

Si lo ejecutas así, aparece un EncodingWarning que te dice en qué línea está el problema. Las líneas señaladas son las candidatas a cambiar de comportamiento en 3.15.

🛠️ Elige una de tres soluciones

① La más recomendada - indicar tú mismo la codificación

Python
# Archivos que crees de ahora en adelante: todo en UTF-8
with open("result.txt", "w", encoding="utf-8") as f:
    f.write("안녕하세요")

# Para leer un archivo que guardaste en cp949 en el pasado
with open("old_data.csv", encoding="cp949") as f:
    print(f.read())

Es la opción más segura. Funciona igual sea cual sea la versión. La respuesta correcta es convertir "indicar siempre la codificación" en un hábito.

② Si quieres mantener el comportamiento anterior

Python
# Indica de forma explícita "sigue el locale del SO" (cp949 en Windows en coreano)
with open("legacy.txt", encoding="locale") as f:
    print(f.read())

③ ¿Tienes prisa? Revierte todo de golpe (solución temporal)

Terminal
# Desactivar el modo UTF-8 con una variable de entorno
set PYTHONUTF8=0        # CMD de Windows

# O con una opción de ejecución
py -3.15 -X utf8=0 my_script.py

La opción ③ es una solución temporal. El paso de Python a UTF-8 no se va a revertir, así que úsala para ganar tiempo y, al final, arréglalo bien con la ①.

💡 El texto coreano roto en Excel es otra historia

Este caso sale mucho en los temas de automatización. Cuando guardas un CSV en UTF-8 pero al abrirlo en Excel el texto en coreano sale roto, es un problema aparte que existe desde hace mucho y no tiene nada que ver con 3.15. Excel no reconoce bien el UTF-8 sin BOM.

Python
import csv

# Para un CSV que abrirás con doble clic en Excel, usa utf-8-sig
with open("보고서.csv", "w", encoding="utf-8-sig", newline="") as f:
    w = csv.writer(f)
    w.writerow(["이름", "부서", "점수"])
    w.writerow(["홍길동", "영업", 92])

utf-8-sig: una codificación que es UTF-8 pero añade una marca BOM al principio del archivo. Excel la ve y piensa: "Ah, esto es UTF-8".

🧩 Cambio 2 - lazy import llega de verdad (PEP 810)

El segundo cambio son las importaciones diferidas explícitas. Se confirmaron en la fase rc y entran tal cual en la versión final.

Python
# Python 3.15+
lazy import json
lazy from pathlib import Path

Una importación marcada con lazy no carga el módulo hasta el momento en que usas ese nombre de verdad. Si tu script tiene un montón de librerías pesadas arriba del todo pero solo las usa en ciertos comandos, el arranque se nota bastante más rápido.

Vamos a desglosarlo 💡

  • lazy: no es una palabra reservada sino una palabra clave suave (soft keyword). Por eso el código existente que usa lazy como nombre de variable no se rompe.
  • Solo en el nivel superior del módulo: si la usas dentro de una función, una clase o un bloque try/except/finally, obtienes un SyntaxError.
  • Combinaciones no permitidas: lazy from module import * y lazy from __future__ import ... no están permitidas.
  • sys.lazy_modules: los módulos diferidos aparecen aquí en lugar de en sys.modules.
  • -X lazy_imports=all: una opción de ejecución que difiere todas las importaciones sin tocar el código. En cambio, normal (el valor por defecto) solo respeta la palabra clave lazy.

⚠️ Ojo con esto

Con lazy import, los fallos de importación explotan más tarde. Normalmente te habrías enterado con un ModuleNotFoundError nada más arrancar el programa, pero ahora salta la primera vez que usas ese módulo.

Si tienes algo como un bot de Discord que funciona 24/7, un programa que no puede morir en silencio de madrugada, te recomiendo usar lazy import con moderación y montar a la vez el manejo de errores y los logs. (Posts relacionados: "Automatización de oficina con Python #13 - Sistema de logs, reintentos y alertas de fallos", "Crear un bot de Discord #25 - Manejo de errores y sistema de logs")

Una cosa más: recuerda que es sintaxis que solo funciona en 3.15. Si escribes lazy en código que tiene que ejecutarse en 3.14 o anterior, es directamente un error de sintaxis. Si necesitas dar soporte a ambas versiones, puedes usar __lazy_modules__.

Python
# Diferido en 3.15, simplemente ignorado en versiones anteriores
__lazy_modules__ = ["json", "pathlib"]

import json      # en 3.15 funciona como lazy

📌 Checklist de migración (cópiala sin problema)

Antes de la versión final, ejecuta estos pasos una vez, en este orden.

Terminal
# 1) Tras instalar 3.15rc2, confirma que convive con tu versión actual
py --list

# 2) Encuentra todo el código que depende de la codificación por defecto
py -3.15 -X warn_default_encoding my_script.py

# 3) Si tienes tests, ejecútalos todos con las advertencias activadas
py -3.15 -X warn_default_encoding -m pytest

# 4) Comprueba si tus librerías principales son compatibles con 3.15
py -3.15 -m pip install -r requirements.txt

Después, revisa a ojo los puntos siguientes.

  • ¿Queda algún sitio donde open() no tenga encoding=?
  • ¿Tienes código que lee archivos que guardaste en cp949 en el pasado? → indica encoding="cp949"
  • ¿Generas CSV que se van a abrir en Excel? → encoding="utf-8-sig"
  • ¿Capturas la salida de comandos de Windows con subprocess.run(..., text=True)? → comprueba si la salida en coreano se rompe
  • ¿Las librerías de tu requirements.txt ya publicaron wheels para 3.15? (Los paquetes con extensiones en C pueden tardar en dar soporte a versiones nuevas)
  • ¿Ya decidiste cuándo subir la versión de Python en tus servidores de despliegue y en GitHub Actions?

📌 Errores frecuentes

1. UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb0 in position 0

  • Causa: 3.15 intentó leer el archivo como UTF-8, pero en realidad es un archivo en coreano guardado en cp949. Es el caso típico de código que funcionaba bien en 3.14 y se rompe por primera vez en 3.15.
  • Solución: indica open(ruta, encoding="cp949"). Volver a guardar el propio archivo en UTF-8 es todavía más limpio.

2. UnicodeDecodeError: 'cp949' codec can't decode byte ...

  • Causa: justo lo contrario de lo anterior. Abriste un archivo UTF-8 sin codificación en Windows con 3.14 o anterior.
  • Solución: indica encoding="utf-8". Si te sale mucho este error, con solo subir a 3.15 desaparecerán muchos de ellos.

3. SyntaxError: lazy import is only allowed at module level

  • Causa: usaste lazy dentro de una función o de un bloque try.
  • Solución: muévelo al principio del archivo. Si necesitas importar de forma condicional, usa un import normal dentro de la función, como antes.

4. El programa funciona bien, pero solo el texto en coreano del archivo de salida sale roto (sin errores)

  • Causa: las codificaciones de lectura y escritura no coinciden, pero no saltó ninguna excepción. Es el tipo más difícil de encontrar.
  • Solución: indica la codificación tanto al leer como al escribir, y repasa tu código con -X warn_default_encoding para asegurarte de que no se te escapa nada.

5. ERROR: Could not build wheels for ... (al instalar en 3.15)

  • Causa: la librería todavía no ha publicado una compilación para 3.15. Es habitual durante unas semanas justo después de la versión final.
  • Solución: si no es urgente, espera. En entornos de trabajo es más seguro no actualizar en cuanto sale la final y migrar solo cuando tus dependencias principales hayan publicado wheels para 3.15.

📌 Para terminar

Aquí va el resumen de hoy 🙌

  • Python 3.15 final sale el 1 de octubre. D-13 a fecha de hoy
  • PEP 686: la codificación por defecto de open() se unifica en UTF-8 sin importar el SO → el mayor cambio para quienes usan Windows en coreano
  • Para revertirlo: encoding="locale", PYTHONUTF8=0, -X utf8=0 — aunque son soluciones temporales
  • PEP 810: lazy import acelera el arranque. Pero solo en el nivel superior del módulo, y es sintaxis exclusiva de 3.15
  • Lo único que tienes que hacer ahora — usar -X warn_default_encoding para encontrar los sitios sin codificación y completarlos

En el próximo post quiero tratar el proceso real de pasarse a 3.15 coincidiendo con la versión final: crear un entorno virtual nuevo, comprobar la compatibilidad de las librerías y migrar los scripts de automatización sin interrupciones. ¡No te lo pierdas! 💬

📚 Posts relacionados

  • Resumen de novedades de Python 3.15 - lazy import, frozendict y UTF-8 por defecto (según la beta 4)
  • ¡Sale Python 3.15 rc1! lazy import confirmado - Todo lo que cambió desde la beta 4
  • ¡Sale Python 3.15 rc2! Última revisión antes de la final (1 de octubre) - Todo lo que cambió desde la rc1
  • Checklist de migración a Python 3.15 final, D-13 ← ¡Estás leyendo este post!
  • Guía completa de entornos virtuales (venv) en Python - De crearlos a activarlos y borrarlos

Autor

Mojangcam

Un registro práctico de desarrollo e IT: código que escribo, servidores que mantengo y los errores que resuelvo por el camino.