3 puntos por GN⁺ 2024-04-28 | 1 comentarios | Compartir por WhatsApp
  • 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=0 o -X utf8=0; para revisar compatibilidad se usan EncodingWarning, encoding="utf-8", encoding="locale" y locale.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() emite EncodingWarning de PEP 597, igual que open()
  • PEP 597 añadió la opción encoding="locale" a TextIOWrapper para poder indicar explícitamente la codificación del locale
  • Antes, incluso si se especificaba encoding="locale" en modo UTF-8, TextIOWrapper usaba "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
    1. Desactivar el modo UTF-8
    2. Usar EncodingWarning de PEP 597 para encontrar los puntos afectados por el modo UTF-8
      • Si se omitió la opción encoding, revisar si corresponde usar encoding="utf-8" o encoding="locale"
      • Si se usó locale.getpreferredencoding(), revisar si corresponde usar "utf-8" o locale.getencoding()
    3. Probar la aplicación en modo UTF-8

Casos previos en Ruby y Java, y alternativas rechazadas

  • Ruby cambió la external_encoding predeterminada 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 EncodingWarning de 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 encoding en todas partes impondría una carga grande a los usuarios, y demasiadas DeprecationWarning pueden 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 PYTHONIOENCODING como codificación predeterminada de los pipes en el módulo subprocess
    • 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=True por encoding="utf-8" o encoding="locale"

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

 
GN⁺ 2024-04-28
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.

    • En Windows, la página de códigos predeterminada del sistema depende no solo de la plataforma, sino también de la configuración regional del sistema.
      Fue un gran error que Windows no ofreciera durante tanto tiempo una forma sencilla de hacer que funciones ANSI como TextOutA usaran 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.
    • Históricamente tenía sentido. La mayoría del software era solo local, y se esperaba que los archivos de texto también estuvieran en una codificación local.
      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 a iso-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.
    • Hace unos días me afectó el cambio implícito de saltos de línea.
      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.
    • Cuando alguien empieza a escribir código en Windows, se topa con este problema varias veces.
  • 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.

    • Estaba usando un .gitignore creado por una función touch hecha 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.
    • La configuración regional global fue un error en general, no solo por la codificación.
      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 charset y 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 regional C.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.

    • Probablemente estés pensando en las cadenas donde el prefijo 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 prefijo u; 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 especificar open('filename', mode='r', encoding='UTF-8') al usar open('filename', mode='r') en entornos donde la codificación predeterminada no es UTF-8, como Windows.
    • En Python 3, el código fuente de Python es UTF-8 por defecto. Pero eso no dice nada sobre la codificación de caracteres que se usa al guardar archivos, y el valor predeterminado depende de la configuración regional.
      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 en tokenize.open, sys.getfilesystemencoding() de os.fsencode, y locale.getpreferredencoding() de open.
  • 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.

    • En Java, la codificación predeterminada al convertir bytes a cadenas originalmente dependía de la plataforma, y ahora es UTF-8.
      Internamente, la clase String sigue usando codificaciones UTF-16 y latin-1, y la JVM usa la codificación UTF-8 modificada como antes. La clase String originalmente 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.
    • Parece que están mezclando la representación interna de las cadenas con la codificación de lectura/escritura.
      Java nunca usó UTF-16 como valor predeterminado para la codificación de lectura/escritura.
    • Parece que cambió hace dos años, en Java 18.
  • ¿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

    • Lo que representa a str es un objeto PyUnicode
      Cuando se solicitan bytes UTF-8, se crea un objeto bytes si es necesario, se cachea como parte de PyUnicode y se libera junto con PyUnicode cuando 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 un PyUnicode, 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 bytes
      Si 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 e seguido 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 parte
  • Me pregunto por qué no utf-8-sig. Maneja la BOM opcional; la semana pasada tuve que corregir un script por eso

    • A estas alturas, nada debería poner una BOM en UTF-8
      Ni siquiera se recomienda, y hoy en día considero razonable que algo falle con una BOM
    • No es buena idea cambiar Python para que agregue silenciosamente una BOM invisible al inicio de toda entrada y salida
  • 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

    • Esto trata sobre qué codificación usar por defecto cuando le pides a Python que abra un archivo “como texto”
      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 conocido
    Sinceramente, pensaba que Python usaba solo UTF-8 en todas partes salvo que se le pidiera explícitamente otra cosa

    • En realidad, depende del caso
      bytes.decode y str.encode han usado UTF-8 por defecto al menos desde Python 3. En cambio, la codificación predeterminada al decodificar nombres de archivo usa sys.getfilesystemencoding(), y en Windows y macOS eso también es UTF-8, pero en Linux depende de la configuración regional, concretamente de CODESET. Por último, open usa directamente locale.getencoding()