PEP 686: modo UTF-8 activado por defecto en Python 3.15
(peps.python.org)- Python 3.15 activará por defecto el modo UTF-8, alineando en UTF-8 la codificación predeterminada de archivos, entrada/salida estándar y pipes
- UTF-8 ya se usa como codificación estándar de facto en archivos fuente, JSON·TOML·YAML, los principales editores, datos web y lenguajes como Node.js·Go·Rust·Java, lo que mejora la interoperabilidad
- La codificación predeterminada anterior varía según la plataforma, por lo que si un desarrollador en Unix omite
encoding="utf-8", pueden aparecer bugs por desajuste en Windows u otros entornos - Si hace falta, puede desactivarse con
PYTHONUTF8=0o-X utf8=0; para revisar compatibilidad se usanEncodingWarning,encoding="utf-8",encoding="locale"ylocale.getencoding() - Los programas que dependen de la codificación predeterminada pueden sufrir, sobre todo en Windows,
UnicodeError, mojibake o corrupción silenciosa de datos, por lo que conviene revisarlos antes
La codificación predeterminada que cambia en Python 3.15
- PEP 686 introduce el cambio de activar por defecto el modo UTF-8 de PEP 540
- Python usará UTF-8 de forma consistente para archivos, stdio y pipes que requieran una codificación predeterminada
- Se activará por defecto a partir de Python 3.15, y los usuarios podrán desactivarlo de las siguientes formas
PYTHONUTF8=0-X utf8=0
Por qué UTF-8 pasa a ser el valor predeterminado
- UTF-8 se ha consolidado como la codificación de texto estándar en muchos entornos
- La codificación predeterminada de los archivos fuente de Python es UTF-8
- JSON, TOML y YAML usan UTF-8
- La mayoría de los editores de texto, incluidos Visual Studio Code y Windows Notepad, usan UTF-8 por defecto
- La mayoría de los sitios web y datos de texto en Internet usan UTF-8
- Muchos lenguajes de programación populares, incluidos Node.js, Go, Rust y Java, usan UTF-8 por defecto
- Si la codificación predeterminada de Python cambia a UTF-8, mejora la interoperabilidad con otras herramientas, lenguajes y formatos de datos
- Muchos desarrolladores de Python en entornos Unix olvidan que la codificación predeterminada depende de la plataforma y, al leer texto UTF-8 como JSON, TOML, Markdown o archivos fuente de Python, omiten
encoding="utf-8" - Las diferencias de codificación predeterminada entre plataformas se convierten en una fuente de bugs cuando ese código se ejecuta en otros entornos
Ajustes en la API de locale y encoding="locale"
- Como el modo UTF-8 afecta a
locale.getpreferredencoding(False), se necesita una API que permita obtener la codificación del locale sin depender del modo UTF-8 locale.getencoding()se añadió con ese propósito: devuelve la codificación del locale ignorando el modo UTF-8- Esta API se añadió en Python 3.11
- Si se especifica la opción
warn_default_encoding,locale.getpreferredencoding()emiteEncodingWarningde PEP 597, igual queopen() - PEP 597 añadió la opción
encoding="locale"aTextIOWrapperpara poder indicar explícitamente la codificación del locale - Antes, incluso si se especificaba
encoding="locale"en modo UTF-8,TextIOWrapperusaba"UTF-8"- Esto no encajaba con la motivación de PEP 597
- Porque no se había previsto el escenario en que el modo UTF-8 pasara a ser el valor predeterminado al cambiar la codificación de texto predeterminada de Python
- Esta inconsistencia se corrigió en Python 3.11, y ahora incluso en modo UTF-8, si se pasa
encoding="locale", se usa la codificación del locale
Compatibilidad hacia atrás y proceso de migración
- Como la mayoría de los sistemas Unix usan locales UTF-8 y Python activa el modo UTF-8 cuando el locale es C o POSIX, el impacto del cambio se concentra principalmente en los usuarios de Windows
- Los programas de Python que dependen de la codificación predeterminada pueden sufrir los siguientes problemas
UnicodeError- mojibake
- corrupción silenciosa de datos
- El procedimiento recomendado para corregir problemas de compatibilidad es el siguiente
- Desactivar el modo UTF-8
- Usar
EncodingWarningde PEP 597 para encontrar los puntos afectados por el modo UTF-8- Si se omitió la opción
encoding, revisar si corresponde usarencoding="utf-8"oencoding="locale" - Si se usó
locale.getpreferredencoding(), revisar si corresponde usar"utf-8"olocale.getencoding()
- Si se omitió la opción
- Probar la aplicación en modo UTF-8
Casos previos en Ruby y Java, y alternativas rechazadas
- Ruby cambió la
external_encodingpredeterminada de Windows a UTF-8 en Ruby 3.0, en 2020, mediante este cambio - Java cambió la codificación de texto predeterminada a UTF-8 en JDK 18, en 2022, mediante este cambio
- Tanto Ruby como Java ofrecen opciones para compatibilidad hacia atrás, pero no proporcionan una advertencia por uso de codificación predeterminada como
EncodingWarningde Python - Se rechazó la opción de desaprobar el uso mismo de la codificación predeterminada
- Hay muchos casos en los que se usa la codificación predeterminada solo para leer y escribir texto ASCII
- Esa advertencia no resulta útil para aplicaciones que no son multiplataforma y solo se ejecutan en Unix
- Obligar a especificar
encodingen todas partes impondría una carga grande a los usuarios, y demasiadasDeprecationWarningpueden hacer que terminen ignorando las advertencias - PEP 387 exige añadir advertencias para cambios que rompen compatibilidad, pero no exige específicamente
DeprecationWarning
- También se rechazó usar
PYTHONIOENCODINGcomo codificación predeterminada de los pipes en el módulosubprocess- Ese enfoque permitiría seguir usando una codificación heredada en
subprocess.Popen(text=True)incluso con el modo UTF-8 activo - Pero complicaría el concepto de “codificación predeterminada”, y ese mismo enfoque también rompe compatibilidad
- Los usuarios pueden desactivar el modo UTF-8 hasta reemplazar
text=Trueporencoding="utf-8"oencoding="locale"
- Ese enfoque permitiría seguir usando una codificación heredada en
Desde la perspectiva de educación al usuario
- Los usuarios nuevos necesitarán aprender menos sobre codificación de texto durante su primer año
- Solo tendrán que aprender sobre codificaciones cuando necesiten manejar archivos de texto que no sean UTF-8
- Los usuarios existentes deberán revisar los puntos afectados siguiendo el procedimiento de compatibilidad hacia atrás
1 comentarios
Opiniones en Hacker News
Siempre fue molesto que la codificación predeterminada de los archivos de texto dependiera de la plataforma, así que este cambio se agradece.
También está bien que no hayan intentado tocar la codificación del sistema de archivos. Ese es otro problema, y bastante complicado por sí mismo.
Fue un gran error que Windows no ofreciera durante tanto tiempo una forma sencilla de hacer que funciones ANSI como
TextOutAusaran la página de códigos UTF-8. Que se pudiera hacer mediante un archivo manifest llegó hacia la mitad del desarrollo de Windows 10, pero esa función debería haber existido desde los tiempos de NT4 o Windows 98.Dependía no solo de la plataforma, sino también de la configuración regional preferida del usuario, y la biblioteca estándar de C funcionaba de la misma manera. Por ejemplo, en Unix/Linux, para idiomas de Europa occidental era común
iso-8859-1, y después de la introducción del euro se volvió frecuente cambiar aiso-8859-15, que incluye el símbolo€. UTF-8 empezó a funcionar sin problemas más o menos a fines de los 2000, y Debian cambió el valor predeterminado a UTF-8 en la versión Etch.En la laptop de la empresa todas las pruebas locales pasaban, pero al desplegar en un host Linux, una aplicación secundaria requería CRLF y no podía consumirlo. Es uno de esos problemas pequeños y tontos que hay que recordar de vez en cuando. Aunque también es una pregunta válida por qué el software recién escrito exige un terminador de línea específico.
Es bueno no depender de valores predeterminados inestables del sistema.
Esos valores suelen terminar devolviendo algo distinto de lo que uno asumía. Hace unos años, trabajando con Ubuntu y scripts de
init.d, un script que lanzaba Java se ejecutaba como root —y más aún porque era antes de Docker— en una shell que no establecía valores predeterminados normales de UTF-8 para usuarios comunes. Como resultado, quedó expuesto el uso de una mala API de Java que tomaba los valores predeterminados del SO.Hoy en día, la mayoría tiene variantes de API que permiten especificar la codificación, y los analizadores estáticos también advierten cuando se usa la incorrecta. Pero si se escapa un solo lugar, el contenido empieza a romperse. Actualmente, usar una codificación que no sea UTF-8 tiene altísimas probabilidades de ser algo no intencional; y si sí fue intencional, debería indicarse explícitamente en lugar de depender de una configuración indirecta rara del SO. Por eso es un buen cambio, y es mejor que el código que se rompa por esto reciba una corrección simple.
.gitignorecreado por una funcióntouchhecha como alias en PowerShell, y por más que intentaba, Git no lo respetaba.Al revisar, resultó que el archivo de texto generado estaba en UTF-16, así que en la práctica se lo ignoraba. Aprendí la lección y cambié el valor predeterminado del sistema a UTF-8, pero ahora simplemente dependo del editor de texto.
Si
printf("%f", 4.2)imprime mágicamente cadenas distintas según el entorno, crea más problemas de los que resuelve. Cuando se quiera un comportamiento dependiente de la configuración regional, hay que pasar explícitamente a la función la información local o las partes relevantes.Hay una heurística que se fue volviendo cada vez más acertada durante las últimas décadas: si en algún lado hay una configuración de
charsety no es UTF-8, está mal.Python 2 era independiente del conjunto de caracteres y siempre funcionaba, pero las mejoras de Python 3 no fueron simplemente mejoras. La forma de distinguir un script de Python 3 de uno de Python 2 es esta: si contiene la cadena
utf-8, es Python 3; si solo funciona con la configuración regionalC.UTF-8, es Python 3. Entiendo este cambio como una forma de “reparar” Python 3, así que lo celebro.Pensé que ya era el valor predeterminado desde Python 3.
u""dejó de ser necesario en Python 3.Acabo de escribir
"éķů"en Python 2.7 y me imprimió los bytes UTF-8 de esos caracteres, así que no tengo del todo claro qué hacía exactamente el prefijou; pero uno de los grandes cambios al pasar de Python 2 a 3 fue que las cadenas pasaron a tener una codificación, mientras que las cadenas de bytes se volvieron secuencias de bytes sin codificación. Este cambio parece tratar principalmente sobre el problema de tener que especificaropen('filename', mode='r', encoding='UTF-8')al usaropen('filename', mode='r')en entornos donde la codificación predeterminada no es UTF-8, como Windows.Como en
Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding"), el literal de cadena, el nombre de archivo y la codificación del contenido del archivo son cosas distintas. Las codificaciones correspondientes son UTF-8 entokenize.open,sys.getfilesystemencoding()deos.fsencode, ylocale.getpreferredencoding()deopen.Eso de que “otros lenguajes de programación populares, incluidos Node.js, Go, Rust y Java, también usan UTF-8 por defecto”... me perdí que Java se hubiera pasado de UTF-16 a UTF-8.
Internamente, la clase
Stringsigue usando codificaciones UTF-16 y latin-1, y la JVM usa la codificación UTF-8 modificada como antes. La claseStringoriginalmente usaba solo UTF-16, pero desde Java 9 también usa una codificación latin-1 de 1 byte por carácter cuando es posible.Java nunca usó UTF-16 como valor predeterminado para la codificación de lectura/escritura.
¿La codificación interna de CPython ahora es UTF-8?
Las cadenas de Python se pueden indexar con subíndices, pero el acceso aleatorio es lo bastante poco frecuente como para que parezca aceptable hacer indexación diferida cuando haga falta. Si solo se avanza o retrocede una posición, no se necesita un índice, así que también sería perfectamente posible usar UTF-8 como representación interna
stres un objetoPyUnicodeCuando se solicitan bytes UTF-8, se crea un objeto
bytessi es necesario, se cachea como parte dePyUnicodey se libera junto conPyUnicodecuando este se libera. Por separado, los puntos de código que componen la cadena se almacenan en un arreglo simple para permitir acceso aleatorio. El tamaño de cada punto de código puede ser de 1, 2 o 4 bytes, y al crear unPyUnicode, si se especifica el valor máximo de punto de código, se redondea a uno de 127, 255, 65535 o 1,114,111, lo que determina si se usan 1/2/4 bytesSi el valor máximo de punto de código es 127, esa representación en arreglo se puede usar directamente como UTF-8. Así que la respuesta a la pregunta es que muchas cadenas cuyos puntos de código son todos de 127 o menos se almacenan como UTF-8. Sin embargo, al recorrer una cadena no hay que hacerlo por punto de código. Un carácter percibido por el usuario, es decir, un clúster de grafemas, está formado por uno o más puntos de código. Por ejemplo, una e acentuada puede ser el punto de código
eseguido de un punto de código de acento combinante, y el emoji de fénix está compuesto por el emoji de ave, un combinador de ancho cero y el emoji de fuego. Algunos sistemas de escritura usados por cientos de millones de personas también funcionan de forma similar, con signos combinantes que representan vocales añadidos a consonantes. Este- -tiene 5 puntos de código, y hay un buen artículo sobre cómo varios lenguajes reportan su “longitud”: https://hsivonen.fi/string-length/. Esto viene de mi experiencia de haber implementado recientemente como extensión C de Python el Unicode TR29, que trata esta parteMe pregunto por qué no
utf-8-sig. Maneja la BOM opcional; la semana pasada tuve que corregir un script por esoNi siquiera se recomienda, y hoy en día considero razonable que algo falle con una BOM
Si hablamos de UTF-8, el framebuffer de Linux ya debería haber tenido soporte real de UTF-8 hace mucho tiempo
Me refiero a soporte real, no a 256/512 glifos. Incluso GNU Hurd tenía una consola de terminal mejor con soporte UTF-8 desde alrededor de 2007, y ya estamos en 2024
Bien. Ahora lo único que falta es que JS cambie a UTF-8
Claro que JS no puede mejorar. A diferencia de cualquier otro lenguaje de programación, tiene que ser compatible con código escrito en 1995
La representación interna de las cadenas es otro tema, y Python, al igual que JavaScript, tampoco usa internamente “simplemente UTF-8”
En “muchos desarrolladores de Python que usan Unix olvidan que la codificación predeterminada depende de la plataforma y omiten
encoding="utf-8"al leer archivos de texto codificados en UTF-8”, quizá no es tanto que lo hayan olvidado, sino que no es algo lo bastante conocidoSinceramente, pensaba que Python usaba solo UTF-8 en todas partes salvo que se le pidiera explícitamente otra cosa
bytes.decodeystr.encodehan usado UTF-8 por defecto al menos desde Python 3. En cambio, la codificación predeterminada al decodificar nombres de archivo usasys.getfilesystemencoding(), y en Windows y macOS eso también es UTF-8, pero en Linux depende de la configuración regional, concretamente deCODESET. Por último,openusa directamentelocale.getencoding()