3 points par GN⁺ 2024-04-28 | 1 commentaires | Partager sur WhatsApp
  • Python 3.15 active par défaut le mode UTF-8, alignant l’encodage par défaut des fichiers, des entrées/sorties standard et des pipes sur UTF-8
  • UTF-8 est utilisé comme encodage de facto standard pour les fichiers source, JSON·TOML·YAML, les principaux éditeurs, les données web, ainsi que Node.js·Go·Rust·Java, ce qui améliore l’interopérabilité
  • L’encodage par défaut actuel varie selon la plateforme ; si des développeurs Unix omettent encoding="utf-8", cela peut provoquer des bugs d’incohérence sur Windows et ailleurs
  • Si nécessaire, il est possible de le désactiver avec PYTHONUTF8=0 ou -X utf8=0 ; pour vérifier la compatibilité, on utilise EncodingWarning, encoding="utf-8", encoding="locale" et locale.getencoding()
  • Les programmes qui dépendent de l’encodage par défaut peuvent subir, principalement sous Windows, des UnicodeError, du mojibake ou une corruption silencieuse des données ; une vérification préalable est donc nécessaire

Ce qui change dans l’encodage par défaut avec Python 3.15

  • PEP 686 est un changement qui active par défaut le mode UTF-8 de PEP 540
  • Pour les fichiers, stdio et pipes qui nécessitent un encodage par défaut, Python utilise systématiquement UTF-8
  • Il est activé par défaut à partir de Python 3.15, et les utilisateurs peuvent le désactiver de la manière suivante
    • PYTHONUTF8=0
    • -X utf8=0

Pourquoi choisir UTF-8 comme valeur par défaut

  • UTF-8 s’est imposé comme l’encodage de texte standard dans de nombreux environnements
    • L’encodage par défaut des fichiers source Python est UTF-8
    • JSON, TOML et YAML utilisent UTF-8
    • La plupart des éditeurs de texte, dont Visual Studio Code et Windows Notepad, utilisent UTF-8 par défaut
    • La majorité des sites web et des données textuelles sur Internet utilisent UTF-8
    • Plusieurs langages de programmation populaires, dont Node.js, Go, Rust et Java, utilisent UTF-8 par défaut
  • Le passage de l’encodage par défaut de Python à UTF-8 améliore l’interopérabilité avec les autres outils, langages et formats de données
  • Beaucoup de développeurs Python en environnement Unix oublient que l’encodage par défaut dépend de la plateforme, et omettent encoding="utf-8" lorsqu’ils lisent du texte UTF-8, par exemple des fichiers JSON, TOML, Markdown ou des fichiers source Python
  • Les différences d’encodage par défaut entre plateformes deviennent alors une cause de bugs, ce code pouvant échouer dans d’autres environnements

Correction de l’API locale et de encoding="locale"

  • Le mode UTF-8 affecte locale.getpreferredencoding(False) ; il faut donc une API permettant d’obtenir l’encodage de la locale indépendamment du mode UTF-8
  • locale.getencoding() a été ajouté à cette fin : il renvoie l’encodage de la locale tout en ignorant le mode UTF-8
    • Cette API a été ajoutée dans Python 3.11
  • Lorsque l’option warn_default_encoding est spécifiée, locale.getpreferredencoding() émet un EncodingWarning de PEP 597, comme open()
  • PEP 597 a ajouté l’option encoding="locale" à TextIOWrapper, afin de pouvoir spécifier explicitement l’encodage de la locale
  • Auparavant, même si encoding="locale" était spécifié en mode UTF-8, TextIOWrapper utilisait "UTF-8"
    • Cela ne correspondait pas à la motivation de PEP 597
    • La raison est que la situation où le mode UTF-8 deviendrait la valeur par défaut lors du changement de l’encodage texte par défaut de Python n’avait pas été anticipée
  • Cette incohérence a été corrigée dans Python 3.11 : même en mode UTF-8, passer encoding="locale" utilise l’encodage de la locale

Rétrocompatibilité et procédure de migration

  • La plupart des systèmes Unix utilisent une locale UTF-8, et Python active le mode UTF-8 lorsque la locale est C ou POSIX ; l’impact du changement se concentre donc principalement sur les utilisateurs Windows
  • Les programmes Python qui dépendent de l’encodage par défaut peuvent rencontrer les problèmes suivants
    • UnicodeError
    • mojibake
    • corruption silencieuse des données
  • La procédure recommandée pour corriger les problèmes de rétrocompatibilité est la suivante
    1. Désactiver le mode UTF-8
    2. Repérer avec l’EncodingWarning de PEP 597 les endroits affectés par le mode UTF-8
      • Lorsque l’option encoding est omise, envisager d’utiliser encoding="utf-8" ou encoding="locale"
      • Lorsque locale.getpreferredencoding() est utilisé, envisager d’utiliser "utf-8" ou locale.getencoding()
    3. Tester l’application en mode UTF-8

Précédents de Ruby·Java et alternatives rejetées

  • Ruby a changé l’external_encoding par défaut de Windows en UTF-8 avec Ruby 3.0, en 2020
  • Java a changé l’encodage texte par défaut en UTF-8 avec JDK 18, en 2022
  • Ruby et Java fournissent tous deux des options de rétrocompatibilité, mais pas d’avertissement sur l’utilisation de l’encodage par défaut comme l’EncodingWarning de Python
  • L’abandon pur et simple de l’utilisation de l’encodage par défaut a été rejeté
    • L’encodage par défaut est souvent utilisé pour lire et écrire uniquement du texte ASCII
    • Pour les applications non multiplateformes qui ne s’exécutent que sous Unix, ce type d’avertissement n’est pas utile
    • Imposer encoding partout ferait peser une charge importante sur les utilisateurs, et de nombreux DeprecationWarning pourraient les amener à ignorer les avertissements
    • PEP 387 exige l’ajout d’un avertissement pour les changements qui cassent la rétrocompatibilité, mais n’exige pas nécessairement un DeprecationWarning
  • L’option consistant à utiliser PYTHONIOENCODING comme encodage par défaut des pipes du module subprocess a également été rejetée
    • Cette approche permettrait à subprocess.Popen(text=True) d’utiliser un encodage hérité même en mode UTF-8
    • Mais elle complexifierait l’« encodage par défaut », et cette approche constituerait elle-même un changement cassant la rétrocompatibilité
    • Les utilisateurs peuvent désactiver le mode UTF-8 jusqu’à ce qu’ils remplacent text=True par encoding="utf-8" ou encoding="locale"

Point de vue de l’éducation des utilisateurs

  • Les nouveaux utilisateurs auront moins besoin d’apprendre les encodages de texte pendant leur première année
  • Ils pourront apprendre les encodages lorsqu’ils devront manipuler des fichiers texte non UTF-8
  • Les utilisateurs existants doivent vérifier les points affectés en suivant la procédure de rétrocompatibilité

1 commentaires

 
GN⁺ 2024-04-28
Avis sur Hacker News
  • Le fait que l’encodage par défaut des fichiers texte varie selon la plateforme a toujours été pénible, donc ce changement est bienvenu
    C’est aussi une bonne chose qu’on n’essaie pas de toucher à l’encodage du système de fichiers. C’est un problème distinct, et lui aussi source de maux de tête

    • Sous Windows, la page de code système par défaut dépend non seulement de la plateforme, mais aussi de la locale système
      Le fait que Windows n’ait longtemps pas fourni de moyen simple pour faire utiliser la page de code UTF-8 aux fonctions ANSI comme TextOutA a été une grosse erreur. Le support via fichier manifest n’est arrivé qu’au milieu du développement de Windows 10, alors qu’une telle fonctionnalité aurait dû exister dès l’époque de NT4 ou Windows 98
    • Historiquement, cela se justifiait. La plupart des logiciels étaient à usage purement local, et on s’attendait donc à ce que les fichiers texte utilisent un encodage local
      Cela dépendait non seulement de la plateforme, mais aussi de la locale préférée de l’utilisateur, et la bibliothèque standard C fonctionne de la même façon. Par exemple, sur Unix/Linux, iso-8859-1 était courant pour les langues d’Europe occidentale, puis avec l’arrivée de l’euro, beaucoup sont passés à iso-8859-15, qui inclut le symbole . L’UTF-8 n’a commencé à fonctionner sans friction que vers la fin des années 2000, et Debian a adopté UTF-8 par défaut avec la version Etch
    • Il y a quelques jours encore, je me suis fait piéger par le fait de modifier implicitement les fins de ligne
      Tous les tests locaux sur le portable de l’entreprise passaient, mais une fois déployé sur un hôte Linux, une sous-application ne pouvait rien traiter parce qu’elle exigeait du CRLF. C’est l’un de ces petits problèmes idiots qu’il faut parfois se rappeler. Cela dit, on peut légitimement se demander pourquoi un logiciel écrit aujourd’hui exige encore un terminateur de ligne particulier
    • Quiconque commence à coder sous Windows s’est fait avoir plusieurs fois par ce problème
  • Ne pas dépendre de valeurs système par défaut instables est une bonne chose
    Ces valeurs ont tendance à se comporter différemment de ce qu’on imagine à un moment ou à un autre. Il y a quelques années, en travaillant avec Ubuntu et des scripts init.d, un script qui lançait Java s’exécutait en root, ce qui était encore plus fréquent avant Docker, dans un shell qui ne configurait pas de valeur UTF-8 correcte par défaut pour les utilisateurs normaux. Cela a mis en évidence l’utilisation d’une mauvaise API Java qui s’appuyait sur la valeur par défaut de l’OS
    Aujourd’hui, la plupart des API ont des variantes qui permettent d’indiquer explicitement l’encodage, et les analyseurs statiques avertissent aussi quand on utilise la mauvaise. Mais il suffit d’un seul oubli pour que le contenu commence à être corrompu. Désormais, l’utilisation d’un encodage autre que l’UTF-8 est très probablement involontaire dans la plupart des cas, et si elle est voulue, il faut l’indiquer explicitement au lieu de s’appuyer sur une configuration implicite et bizarre de l’OS. Donc c’est un bon changement, et mieux vaut que le code cassé ici reçoive un correctif simple

    • J’utilisais un .gitignore créé par une fonction touch définie comme alias dans PowerShell, mais Git refusait obstinément de le respecter
      Après vérification, le fichier texte créé était en UTF-16 et était donc en pratique ignoré. J’en ai tiré la leçon et j’ai changé la valeur système par défaut pour UTF-8, mais aujourd’hui je m’en remets simplement à un éditeur de texte
    • La locale globale était une erreur de manière générale, pas seulement pour l’encodage
      Si printf("%f", 4.2) produit comme par magie une chaîne différente selon l’environnement, cela crée plus de problèmes que cela n’en résout. Quand on veut un comportement dépendant de la locale, il faut passer explicitement à la fonction les informations de locale, ou du moins la partie pertinente
  • Depuis quelques décennies, une heuristique s’est progressivement imposée : s’il y a un réglage charset quelque part, alors s’il n’est pas en UTF-8, c’est faux
    Python 2 était indifférent au jeu de caractères et fonctionnait donc toujours, mais les améliorations de Python 3 n’étaient pas uniquement des améliorations simples. Voici comment distinguer un script Python 3 d’un script Python 2 : s’il contient la chaîne utf-8, c’est du Python 3, et s’il ne fonctionne que dans une locale C.UTF-8, c’est du Python 3. Ce changement est bienvenu parce qu’il donne l’impression de “réparer” Python 3

  • Je pensais que c’était déjà la valeur par défaut depuis Python 3

    • Tu penses sans doute aux chaînes pour lesquelles le préfixe u"" est devenu inutile en Python 3
      Je viens d’essayer de saisir "éķů" dans Python 2.7, et il affiche les octets UTF-8 correspondants, donc je ne suis pas certain de ce que faisait exactement le préfixe u, mais l’un des grands changements entre Python 2 et 3 est que les chaînes ont un encodage, tandis que les chaînes d’octets sont devenues de simples séquences d’octets sans encodage. Ce changement semble surtout concerner le fait que, dans des environnements où l’encodage par défaut n’est pas UTF-8, comme Windows, il fallait indiquer explicitement open('filename', mode='r', encoding='UTF-8') au lieu de simplement utiliser open('filename', mode='r')
    • En Python 3, le code source Python est en UTF-8 par défaut. Mais cela ne dit rien de l’encodage des caractères utilisé lors de l’enregistrement dans un fichier, dont la valeur par défaut dépend toujours de la locale
      Comme dans Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding"), les littéraux de chaînes, les noms de fichiers et le contenu des fichiers relèvent chacun d’encodages différents. Les encodages correspondants sont l’UTF-8 de tokenize.open, le sys.getfilesystemencoding() de os.fsencode, et le locale.getpreferredencoding() de open
  • « D’autres langages de programmation populaires, dont Node.js, Go, Rust et Java, utilisent eux aussi l’UTF-8 par défaut » : j’ai raté le moment où Java est passé de l’UTF-16 à l’UTF-8

    • En Java, l’encodage par défaut pour convertir des octets en chaîne dépendait autrefois de la plateforme, et c’est maintenant UTF-8
      À l’intérieur de la classe String, les encodages UTF-16 et latin-1 sont toujours utilisés, et la JVM continue d’utiliser comme auparavant un encodage UTF-8 modifié. À l’origine, la classe String utilisait uniquement UTF-16, mais depuis Java 9, elle utilise aussi un encodage latin-1 sur 1 octet par caractère quand c’est possible
    • Cela semble mélanger représentation interne des chaînes et encodage de lecture/écriture
      Java n’a jamais utilisé UTF-16 comme valeur par défaut pour l’encodage de lecture/écriture
    • Apparemment, cela a changé avec Java 18, il y a deux ans
  • L’encodage interne de CPython est-il désormais UTF-8 ?
    Les chaînes Python peuvent être indexées par position, mais l’accès aléatoire est suffisamment rare pour qu’une indexation paresseuse au besoin semble acceptable. S’il suffit d’avancer ou de reculer d’un caractère, pas besoin d’index, donc une représentation interne en UTF-8 paraît tout à fait possible

    • C’est l’objet PyUnicode qui représente un str
      Quand les octets UTF-8 sont demandés, un objet bytes est créé si nécessaire, mis en cache comme partie de PyUnicode, puis libéré en même temps que PyUnicode. Séparément, les points de code qui composent la chaîne sont stockés dans un tableau simple afin de permettre l’accès aléatoire. Chaque point de code peut occuper 1, 2 ou 4 octets, et lors de la création du PyUnicode, la valeur maximale de point de code indiquée est arrondie à l’une des valeurs 127, 255, 65535 ou 1 114 111, ce qui détermine l’usage de 1/2/4 octets.
      Si la valeur maximale de point de code est 127, cette représentation en tableau peut être utilisée directement comme UTF-8. Donc la réponse à la question est que beaucoup de chaînes dont tous les points de code sont inférieurs ou égaux à 127 sont bien stockées en UTF-8. En revanche, il ne faut pas parcourir une chaîne au niveau des points de code. Les caractères perçus par l’utilisateur, c’est-à-dire les grapheme clusters, sont composés d’un ou plusieurs points de code. Par exemple, un e accentué peut être composé du point de code e suivi d’un point de code d’accent combinant, et l’emoji phénix est constitué de l’emoji oiseau, d’un liant sans chasse et de l’emoji feu. Certains systèmes d’écriture utilisés par des centaines de millions de personnes fonctionnent aussi de manière similaire, avec des signes combinants indiquant les voyelles attachés aux consonnes. Ce - - contient 5 points de code, et il existe un bon article sur la façon dont plusieurs langages rapportent cette « longueur » : https://hsivonen.fi/string-length/. Cela vient de mon expérience récente à implémenter Unicode TR29 en extension C pour Python
  • Je me demande pourquoi pas utf-8-sig. Ça gère le BOM optionnel, et j’ai encore dû corriger un script à cause de ça la semaine dernière

    • Désormais, plus rien ne devrait mettre de BOM dans de l’UTF-8
      Ce n’est pas recommandé, et de nos jours je considère même comme raisonnable un comportement qui échoue à cause d’un BOM
    • Ce ne serait pas une bonne idée de modifier Python pour qu’il préfixe silencieusement toutes les entrées/sorties d’un BOM invisible
  • Puisqu’on parle d’UTF-8, le framebuffer Linux aurait dû avoir un vrai support UTF-8 depuis longtemps
    Un vrai support, pas les consoles limitées à 256/512 glyphes. Même GNU Hurd avait déjà une meilleure console terminal avec support UTF-8 vers 2007, et on est en 2024

  • Bien. Maintenant, il ne reste plus qu’à faire passer JS à l’UTF-8
    Bien sûr, JS ne peut pas être amélioré. Contrairement à tout autre langage de programmation, il doit rester compatible avec du code écrit en 1995

    • Ici, on parle de l’encodage utilisé par défaut quand on demande à Python d’ouvrir un fichier « en texte »
      La représentation interne des chaînes est une question distincte, et comme JavaScript, Python n’utilise pas simplement « de l’UTF-8 » en interne
  • À propos de « de nombreux développeurs Python sur Unix oublient que l’encodage par défaut dépend de la plateforme et omettent encoding="utf-8" lorsqu’ils lisent des fichiers texte encodés en UTF-8 », ce n’est peut-être pas tant un oubli qu’un fait insuffisamment connu
    Honnêtement, je pensais que Python utilisait uniquement UTF-8 partout, sauf demande explicite du contraire

    • En pratique, cela dépend du cas
      bytes.decode et str.encode utilisent par défaut UTF-8 au moins depuis Python 3. En revanche, l’encodage par défaut pour décoder les noms de fichiers utilise sys.getfilesystemencoding(), qui vaut aussi UTF-8 sous Windows et macOS, mais dépend de la locale — plus précisément de CODESET — sous Linux. Enfin, open utilise directement locale.getencoding()