3 Punkte von GN⁺ 2024-04-28 | 1 Kommentare | Auf WhatsApp teilen
  • Python 3.15 aktiviert den UTF-8-Modus standardmäßig und setzt damit die Standardkodierung für Dateien, Standard-Ein-/Ausgabe und Pipes auf UTF-8
  • UTF-8 wird bereits als De-facto-Standardkodierung für Quelldateien, JSON·TOML·YAML, wichtige Editoren, Webdaten sowie Node.js·Go·Rust·Java verwendet, was die Interoperabilität verbessert
  • Die bisherige Standardkodierung unterschied sich je nach Plattform, sodass bei Unix-Entwicklern, die encoding="utf-8" weglassen, auf Windows und anderen Systemen Mismatch-Bugs entstehen können
  • Bei Bedarf lässt sich das mit PYTHONUTF8=0 oder -X utf8=0 deaktivieren; für Kompatibilitätsprüfungen kommen EncodingWarning, encoding="utf-8", encoding="locale" und locale.getencoding() zum Einsatz
  • Programme, die sich auf die Standardkodierung verlassen, können vor allem unter Windows UnicodeError, Mojibake oder stille Datenbeschädigung verursachen und sollten daher vorab geprüft werden

Die geänderte Standardkodierung in Python 3.15

  • PEP 686 führt die Änderung ein, den UTF-8-Modus aus PEP 540 standardmäßig zu aktivieren
  • Für Dateien, stdio und Pipes, bei denen eine Standardkodierung benötigt wird, verwendet Python konsistent UTF-8
  • Ab Python 3.15 ist dies standardmäßig aktiv; Nutzer können es auf folgende Weise deaktivieren
    • PYTHONUTF8=0
    • -X utf8=0

Warum UTF-8 zum Standard wird

  • UTF-8 hat sich in vielen Umgebungen als Standard für Textkodierung etabliert
    • Die Standardkodierung von Python-Quelldateien ist UTF-8
    • JSON, TOML und YAML verwenden UTF-8
    • Die meisten Texteditoren, darunter Visual Studio Code und Windows Notepad, verwenden standardmäßig UTF-8
    • Die meisten Websites und Textdaten im Internet verwenden UTF-8
    • Viele populäre Programmiersprachen, darunter Node.js, Go, Rust und Java, verwenden standardmäßig UTF-8
  • Wenn Pythons Standardkodierung auf UTF-8 umgestellt wird, verbessert das die Interoperabilität mit anderen Tools, Sprachen und Datenformaten
  • Viele Python-Entwickler in Unix-Umgebungen vergessen, dass die Standardkodierung plattformabhängig ist, und lassen beim Lesen von UTF-8-Texten wie JSON-, TOML-, Markdown- oder Python-Dateien encoding="utf-8" weg
  • Unterschiede bei den Standardkodierungen je nach Plattform werden dadurch zu einer Bug-Ursache, wenn solcher Code in anderen Umgebungen ausgeführt wird

Anpassungen an der locale-API und encoding="locale"

  • Da der UTF-8-Modus locale.getpreferredencoding(False) beeinflusst, wird eine API benötigt, die die locale-Kodierung unabhängig vom UTF-8-Modus liefert
  • locale.getencoding() wurde zu diesem Zweck ergänzt und gibt die locale-Kodierung zurück, ignoriert dabei jedoch den UTF-8-Modus
    • Diese API wurde in Python 3.11 hinzugefügt
  • Wenn die Option warn_default_encoding gesetzt ist, erzeugt locale.getpreferredencoding() wie open() ein EncodingWarning aus PEP 597
  • PEP 597 hat für TextIOWrapper die Option encoding="locale" ergänzt, damit die locale-Kodierung explizit angegeben werden kann
  • Zuvor verwendete TextIOWrapper im UTF-8-Modus selbst bei Angabe von encoding="locale" "UTF-8"
    • Das entsprach nicht der Motivation von PEP 597
    • Der Fall, dass der UTF-8-Modus standardmäßig aktiv wird, wenn sich Pythons Standard-Textkodierung ändert, war damals nicht vorhergesehen worden
  • Diese Inkonsistenz wurde in Python 3.11 behoben; auch im UTF-8-Modus wird bei Übergabe von encoding="locale" die locale-Kodierung verwendet

Abwärtskompatibilität und Migrationsschritte

  • Da die meisten Unix-Systeme eine UTF-8-locale verwenden und Python den UTF-8-Modus aktiviert, wenn die locale C oder POSIX ist, betrifft die Änderung vor allem Windows-Nutzer
  • Python-Programme, die von der Standardkodierung abhängen, können folgende Probleme verursachen
    • UnicodeError
    • Mojibake
    • stille Datenbeschädigung
  • Das empfohlene Vorgehen zur Behebung von Abwärtskompatibilitätsproblemen ist wie folgt
    1. Den UTF-8-Modus deaktivieren
    2. Mit EncodingWarning aus PEP 597 die Stellen finden, die vom UTF-8-Modus beeinflusst werden
      • Wenn die Option encoding weggelassen wurde, den Einsatz von encoding="utf-8" oder encoding="locale" prüfen
      • Wenn locale.getpreferredencoding() verwendet wurde, den Einsatz von "utf-8" oder locale.getencoding() prüfen
    3. Die Anwendung im UTF-8-Modus testen

Vorbilder aus Ruby und Java sowie abgelehnte Alternativen

  • Ruby hat in Ruby 3.0 im Jahr 2020 die Standard-external_encoding unter Windows auf UTF-8 umgestellt
  • Java hat in JDK 18 im Jahr 2022 die Standard-Textkodierung auf UTF-8 umgestellt
  • Sowohl Ruby als auch Java bieten Optionen für Abwärtskompatibilität, aber keine Warnung zur Nutzung der Standardkodierung wie Pythons EncodingWarning
  • Die Abschaffung der Nutzung einer Standardkodierung insgesamt wurde abgelehnt
    • Häufig wird die Standardkodierung genutzt, um nur ASCII-Text zu lesen oder zu schreiben
    • Für Anwendungen, die nur unter Unix laufen und nicht plattformübergreifend sind, wäre eine solche Warnung nicht nützlich
    • Wenn encoding überall erzwungen würde, wäre die Belastung für Nutzer groß, und viele DeprecationWarning könnten dazu führen, dass Warnungen generell ignoriert werden
    • PEP 387 verlangt zwar Warnungen bei Änderungen, die die Abwärtskompatibilität brechen, aber nicht zwingend DeprecationWarning
  • Auch die Idee, PYTHONIOENCODING als Standardkodierung für Pipes im Modul subprocess zu verwenden, wurde abgelehnt
    • Dadurch wäre es möglich, selbst im UTF-8-Modus für subprocess.Popen(text=True) eine Legacy-Kodierung zu verwenden
    • Das würde jedoch die „Standardkodierung“ unnötig kompliziert machen, und auch dieser Ansatz wäre selbst eine Änderung mit Auswirkungen auf die Abwärtskompatibilität
    • Nutzer können den UTF-8-Modus deaktivieren, bis sie text=True durch encoding="utf-8" oder encoding="locale" ersetzt haben

Perspektive der Nutzerbildung

  • Neue Nutzer müssen im ersten Jahr seltener etwas über Textkodierungen lernen
  • Erst beim Umgang mit Textdateien, die nicht UTF-8 verwenden, müssen sie sich mit Kodierungen beschäftigen
  • Bestehende Nutzer sollten die betroffenen Stellen gemäß dem Kompatibilitätsverfahren prüfen

1 Kommentare

 
GN⁺ 2024-04-28
Meinungen auf Hacker News
  • Dass die Standardkodierung für Textdateien je nach Plattform unterschiedlich ist, war schon immer nervig; diese Änderung ist daher willkommen.
    Gut ist auch, dass man nicht versucht hat, gleich die Dateisystem-Kodierung anzufassen. Das ist ein eigenes Problem und auf seine Weise mühsam.

    • Unter Windows hängt die systemweite Standard-Codepage nicht nur von der Plattform ab, sondern auch von der System-Locale.
      Es war ein großer Fehler, dass Windows lange keine einfache Möglichkeit bot, ANSI-Funktionen wie TextOutA die UTF-8-Codepage verwenden zu lassen. Dass es per Manifest-Datei möglich wurde, kam erst irgendwann mitten in der Entwicklung von Windows 10; so eine Funktion hätte schon zu Zeiten von NT4 oder Windows 98 vorhanden sein müssen.
    • Historisch ergab das Sinn. Die meiste Software war nur lokal gedacht, und man erwartete, dass Textdateien in der lokalen Kodierung vorliegen.
      Es hing nicht nur von der Plattform ab, sondern auch von der bevorzugten Locale des Nutzers, und die C-Standardbibliothek verhielt sich genauso. Unter Unix/Linux war in westeuropäischen Sprachen zum Beispiel iso-8859-1 verbreitet; nach Einführung des Euro wechselte man oft zu iso-8859-15 mit dem Zeichen . Dass UTF-8 problemlos zu funktionieren begann, war etwa Ende der 2000er der Fall, und Debian stellte mit dem Etch-Release den Standard auf UTF-8 um.
    • Vor ein paar Tagen bin ich daran gescheitert, dass Zeilenumbrüche implizit geändert wurden.
      Auf dem Firmenlaptop liefen alle lokalen Tests, aber nach dem Deployment auf einen Linux-Host konnte eine nachgelagerte Anwendung die Daten nicht verarbeiten, weil sie CRLF verlangte. Eines dieser kleinen, dummen Probleme, an die man gelegentlich denken muss. Allerdings ist es auch eine berechtigte Frage, warum neu geschriebene Software einen bestimmten Zeilenabschluss verlangt.
    • Sobald jemand unter Windows anfing, Code zu schreiben, ist man mehrfach in dieses Problem gelaufen.
  • Es ist gut, sich nicht auf instabile Systemstandards zu verlassen.
    Solche Werte neigen dazu, irgendwann anders zurückzukommen, als man angenommen hatte. Vor ein paar Jahren, als ich mit Ubuntu und init.d-Skripten zu tun hatte, lief ein Skript, das Java startete, als root – und das war noch vor Docker, also umso mehr –, in einer Shell, die für normale Nutzer keine vernünftigen UTF-8-Defaults setzte. Dadurch wurde eine schlechte Verwendung einer Java-API sichtbar, die den OS-Standard nutzte.
    Heutzutage gibt es für die meisten Dinge API-Varianten, bei denen man die Kodierung explizit angeben kann, und statische Code-Analyzer warnen, wenn man die falsche verwendet. Aber wenn nur eine Stelle fehlt, beginnt der Inhalt kaputtzugehen. Inzwischen ist die Verwendung einer Nicht-UTF-8-Kodierung sehr wahrscheinlich meist unbeabsichtigt; wenn sie beabsichtigt ist, sollte man sie explizit angeben, statt sich auf merkwürdige indirekte OS-Einstellungen zu verlassen. Daher ist das eine gute Änderung, und Code, der dadurch bricht, sollte lieber mit einer einfachen Korrektur versehen werden.

    • Ich hatte eine .gitignore verwendet, die von einer als Alias angelegten touch-Funktion in PowerShell erzeugt wurde, und Git wollte sie einfach nicht beachten.
      Beim Nachsehen stellte sich heraus, dass die erzeugte Textdatei UTF-16 war und deshalb praktisch ignoriert wurde. Daraus habe ich gelernt und den Systemstandard auf UTF-8 umgestellt, aber inzwischen verlasse ich mich einfach auf den Texteditor.
    • Globale Locales waren nicht nur bei Kodierungen, sondern generell ein Fehler.
      Wenn printf("%f", 4.2) je nach Umgebung auf magische Weise andere Strings ausgibt, verursacht das mehr Probleme, als es löst. Wenn man Locale-abhängiges Verhalten will, sollte man die Locale-Informationen oder die relevanten Teile explizit an die Funktion übergeben.
  • Es gibt eine Heuristik, die in den letzten Jahrzehnten immer zutreffender geworden ist: Wenn irgendwo eine charset-Einstellung steht und sie nicht UTF-8 ist, ist sie falsch.
    Python 2 war gegenüber Zeichensätzen indifferent und funktionierte daher immer, aber die Verbesserungen in Python 3 waren nicht einfach nur Verbesserungen. So erkennt man ein Python-3-Skript im Unterschied zu einem Python-2-Skript: Wenn der String utf-8 darin vorkommt, ist es Python 3, und wenn es nur in einer C.UTF-8-Locale läuft, ist es Python 3. Diese Änderung wirkt wie eine „Reparatur“ von Python 3 und ist daher willkommen.

  • Ich dachte, das sei seit Python 3 der Standard.

    • Vermutlich denkst du an Strings, bei denen das Präfix u"" in Python 3 unnötig wurde.
      Ich habe gerade in Python 2.7 "éķů" eingegeben, und es gab die UTF-8-Bytes dieser Zeichen aus; daher bin ich mir nicht ganz sicher, was das u-Präfix genau bewirkte. Aber eine der großen Änderungen beim Wechsel von Python 2 zu 3 war, dass Strings eine Kodierung haben und Byte-Strings zu Byte-Sequenzen ohne Kodierung wurden. Bei dieser Änderung scheint es vor allem um Umgebungen wie Windows zu gehen, in denen die Standardkodierung nicht UTF-8 ist und man bei open('filename', mode='r') explizit open('filename', mode='r', encoding='UTF-8') angeben musste.
    • In Python 3 ist Python-Quellcode standardmäßig UTF-8. Über die Zeichenkodierung, die beim Speichern in Dateien verwendet wird, sagt das aber nichts aus; der Standard hängt von der Locale ab.
      Wie bei Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding") sind die Kodierungen für String-Literale, Dateinamen und Dateiinhalte jeweils unterschiedlich. Die entsprechenden Kodierungen sind UTF-8 bei tokenize.open, sys.getfilesystemencoding() von os.fsencode und locale.getpreferredencoding() bei open.
  • „Auch andere beliebte Programmiersprachen, darunter Node.js, Go, Rust und Java, verwenden standardmäßig UTF-8“ – da habe ich wohl verpasst, dass Java von UTF-16 zu UTF-8 gewechselt ist.

    • Die Standardkodierung beim Umwandeln von Bytes in Strings war in Java ursprünglich plattformabhängig und ist jetzt UTF-8.
      Intern verwendet die Klasse String weiterhin UTF-16- und latin-1-Kodierungen, und die JVM nutzt wie früher eine modifizierte UTF-8-Kodierung. Die Klasse String verwendete ursprünglich nur UTF-16, nutzt aber seit Java 9, wenn möglich, auch eine latin-1-Kodierung mit 1 Byte pro Zeichen.
    • Es klingt, als würden hier die interne String-Repräsentation und die Kodierung beim Lesen/Schreiben vermischt.
      Java hat nie UTF-16 als Standard für die Lese-/Schreibkodierung verwendet.
    • Sieht so aus, als sei das vor zwei Jahren mit Java 18 geändert worden.
  • Ist die interne Kodierung von CPython jetzt UTF-8?
    Python-Strings lassen sich per Index adressieren, aber Random Access ist selten genug, dass Lazy Indexing bei Bedarf wohl in Ordnung wäre. Wenn man nur ein Zeichen vor- oder zurückgeht, braucht man keinen Index; daher wäre eine interne Darstellung als UTF-8 durchaus möglich.

    • Die Darstellung von str erfolgt durch ein PyUnicode-Objekt.
      Wenn UTF-8-Bytes angefordert werden, wird bei Bedarf ein bytes-Objekt erzeugt, als Teil von PyUnicode zwischengespeichert und zusammen mit PyUnicode freigegeben. Separat werden die Codepoints, aus denen der String besteht, in einem einfachen Array gespeichert, damit Random Access möglich ist. Jeder Codepoint kann 1, 2 oder 4 Byte groß sein; wenn beim Erzeugen von PyUnicode der maximale Codepoint-Wert angegeben wird, wird er auf einen der Werte 127, 255, 65535 oder 1.114.111 aufgerundet, wodurch entschieden wird, ob 1/2/4 Byte verwendet werden.
      Wenn der maximale Codepoint-Wert 127 ist, kann diese Array-Darstellung direkt als UTF-8 verwendet werden. Die Antwort auf die Frage lautet also: Viele Strings, deren Codepoints alle höchstens 127 sind, werden als UTF-8 gespeichert. Allerdings sollte man beim Iterieren über Strings nicht in Codepoints denken. Ein vom Nutzer wahrgenommenes Zeichen, also ein Graphem-Cluster, besteht aus einem oder mehreren Codepoints. Ein e mit Akzent kann zum Beispiel aus dem Codepoint e gefolgt von einem kombinierenden Akzent-Codepoint bestehen, und das Phönix-Emoji setzt sich aus einem Vogel-Emoji, einem Zero-Width-Joiner und einem Feuer-Emoji zusammen. Auch einige Schriftsysteme, die von Hunderten Millionen Menschen verwendet werden, funktionieren ähnlich: An Konsonanten werden kombinierende Zeichen für Vokale angehängt. Dieses - - besteht aus 5 Codepoints, und es gibt einen guten Artikel darüber, wie verschiedene Sprachen dessen „Länge“ melden: https://hsivonen.fi/string-length/. Das stammt aus der Erfahrung, gerade Unicode TR29, das diesen Bereich behandelt, als Python-C-Erweiterung implementiert zu haben.
  • Ich frage mich, warum nicht utf-8-sig. Das würde eine optionale BOM behandeln; erst letzte Woche musste ich wegen genau so etwas ein Skript anpassen.

    • Heutzutage sollte nichts mehr eine BOM in UTF-8 einfügen.
      Es wird auch nicht empfohlen, und ich finde es heutzutage vertretbar, an einer BOM zu scheitern.
    • Python so zu ändern, dass es stillschweigend vor jede Ein- und Ausgabe eine unsichtbare BOM setzt, ist keine gute Idee.
  • Wenn es um UTF-8 geht: Der Linux-Framebuffer hätte schon vor langer Zeit ordentliche UTF-8-Unterstützung bekommen müssen.
    Ich meine echte Unterstützung, nicht nur 256/512 Glyphen. Sogar GNU Hurd hatte etwa seit 2007 eine bessere Terminal-Konsole mit UTF-8-Unterstützung, und jetzt haben wir 2024.

  • Gut. Jetzt bleibt nur noch, dass JS auf UTF-8 umstellt.
    Natürlich kann JS nicht verbessert werden. Anders als jede andere Programmiersprache muss es ja mit Code kompatibel bleiben, der 1995 geschrieben wurde.

    • Hier geht es darum, welche Kodierung Python standardmäßig verwenden soll, wenn man eine Datei „als Text“ öffnen lässt.
      Die interne Darstellung von Strings ist ein anderes Thema, und genau wie JavaScript verwendet auch Python intern nicht „einfach UTF-8“.
  • Zu „Viele Python-Entwickler, die Unix verwenden, vergessen, dass die Standardkodierung plattformabhängig ist, und lassen encoding="utf-8" weg, wenn sie UTF-8-kodierte Textdateien lesen“: Vielleicht haben sie es nicht vergessen, sondern es ist einfach nicht bekannt genug.
    Ehrlich gesagt dachte ich, Python verwende überall ausschließlich UTF-8, sofern man nicht ausdrücklich etwas anderes verlangt.

    • Tatsächlich hängt es vom Fall ab.
      bytes.decode und str.encode verwenden zumindest seit Python 3 standardmäßig UTF-8. Beim Dekodieren von Dateinamen dagegen wird standardmäßig sys.getfilesystemencoding() verwendet; unter Windows und macOS ist das ebenfalls UTF-8, unter Linux hängt es jedoch vom Locale ab, genauer von CODESET. Und schließlich verwendet open direkt locale.getencoding().