3 pontos por GN⁺ 2024-04-28 | 1 comentários | Compartilhar no WhatsApp
  • O Python 3.15 ativa o modo UTF-8 por padrão, alinhando a codificação padrão de arquivos, entrada/saída padrão e pipes para UTF-8
  • UTF-8 é usado como codificação padrão de fato em arquivos-fonte, JSON·TOML·YAML, principais editores, dados da Web e linguagens como Node.js·Go·Rust·Java, aumentando a interoperabilidade
  • A codificação padrão anterior variava conforme a plataforma; quando desenvolvedores Unix omitiam encoding="utf-8", podiam surgir bugs de inconsistência no Windows e em outros ambientes
  • Se necessário, é possível desativar com PYTHONUTF8=0 ou -X utf8=0; para verificar compatibilidade, usam-se EncodingWarning, encoding="utf-8", encoding="locale" e locale.getencoding()
  • Programas que dependem da codificação padrão podem sofrer principalmente no Windows com UnicodeError, mojibake e corrupção silenciosa de dados, exigindo verificação prévia

A codificação padrão que muda no Python 3.15

  • A PEP 686 é uma mudança que ativa por padrão o modo UTF-8 da PEP 540
  • Em arquivos, stdio e pipes que precisam de uma codificação padrão, o Python passa a usar UTF-8 de forma consistente
  • A ativação por padrão começa no Python 3.15, e usuários podem desativá-la das seguintes formas
    • PYTHONUTF8=0
    • -X utf8=0

Por que tornar UTF-8 o padrão

  • UTF-8 se consolidou como a codificação de texto padrão em vários ambientes
    • A codificação padrão de arquivos-fonte Python é UTF-8
    • JSON, TOML e YAML usam UTF-8
    • A maioria dos editores de texto, incluindo Visual Studio Code e Windows Notepad, usa UTF-8 por padrão
    • A maioria dos sites e dados de texto da internet usa UTF-8
    • Várias linguagens de programação populares, incluindo Node.js, Go, Rust e Java, usam UTF-8 por padrão
  • Quando a codificação padrão do Python muda para UTF-8, melhora a interoperabilidade com outras ferramentas, linguagens e formatos de dados
  • Muitos desenvolvedores Python em ambientes Unix esquecem que a codificação padrão depende da plataforma e omitem encoding="utf-8" ao ler textos em UTF-8, como JSON·TOML·Markdown·arquivos-fonte Python
  • Diferenças de codificação padrão entre plataformas se tornam uma causa de bugs que quebram esse código em outros ambientes

Correções na API de locale e em encoding="locale"

  • O modo UTF-8 afeta locale.getpreferredencoding(False), portanto é necessária uma API para obter a codificação do locale independentemente do modo UTF-8
  • locale.getencoding() foi adicionada para esse propósito e retorna a codificação do locale, ignorando o modo UTF-8
    • Essa API foi adicionada no Python 3.11
  • Quando a opção warn_default_encoding é especificada, locale.getpreferredencoding() emite o EncodingWarning da PEP 597, assim como open()
  • A PEP 597 adicionou a opção encoding="locale" ao TextIOWrapper, permitindo especificar explicitamente a codificação do locale
  • Antes, mesmo ao especificar encoding="locale" no modo UTF-8, o TextIOWrapper usava "UTF-8"
    • Isso não estava alinhado com a motivação da PEP 597
    • O motivo foi que não se havia previsto a situação em que o modo UTF-8 se tornaria o padrão quando a codificação de texto padrão do Python mudasse
  • Essa inconsistência foi corrigida no Python 3.11; mesmo no modo UTF-8, ao passar encoding="locale", a codificação do locale é usada

Compatibilidade retroativa e procedimento de migração

  • A maioria dos sistemas Unix usa locale UTF-8, e o Python ativa o modo UTF-8 quando o locale é C ou POSIX, portanto o impacto da mudança se concentra principalmente em usuários do Windows
  • Programas Python que dependem da codificação padrão podem enfrentar os seguintes problemas
    • UnicodeError
    • mojibake
    • corrupção silenciosa de dados
  • O procedimento recomendado para corrigir problemas de compatibilidade retroativa é o seguinte
    1. Desativar o modo UTF-8
    2. Usar o EncodingWarning da PEP 597 para encontrar pontos afetados pelo modo UTF-8
      • Quando a opção encoding for omitida, considerar usar encoding="utf-8" ou encoding="locale"
      • Quando locale.getpreferredencoding() for usado, considerar usar "utf-8" ou locale.getencoding()
    3. Testar a aplicação no modo UTF-8

Casos anteriores em Ruby·Java e alternativas rejeitadas

  • O Ruby alterou o external_encoding padrão no Windows para UTF-8 no Ruby 3.0, em 2020
  • O Java alterou a codificação de texto padrão para UTF-8 no JDK 18, em 2022
  • Tanto Ruby quanto Java oferecem opções para compatibilidade retroativa, mas não fornecem um alerta de uso da codificação padrão como o EncodingWarning do Python
  • A proposta de abandonar o próprio uso da codificação padrão foi rejeitada
    • Há muitos casos em que a codificação padrão é usada apenas para ler e escrever texto ASCII
    • Em aplicações não multiplataforma que rodam apenas em Unix, esse tipo de alerta não é útil
    • Obrigar encoding em todos os lugares impõe uma carga grande aos usuários, e muitos DeprecationWarning podem fazer com que os usuários ignorem os avisos
    • A PEP 387 exige a adição de avisos para mudanças que quebram compatibilidade retroativa, mas não exige necessariamente DeprecationWarning
  • A proposta de usar PYTHONIOENCODING como codificação padrão dos pipes do módulo subprocess também foi rejeitada
    • Essa abordagem permitiria usar uma codificação legada em subprocess.Popen(text=True) mesmo no modo UTF-8
    • Mas ela tornaria a “codificação padrão” mais complexa, e a própria abordagem também é uma mudança que quebra compatibilidade retroativa
    • Usuários podem desativar o modo UTF-8 até trocar text=True por encoding="utf-8" ou encoding="locale"

Do ponto de vista da educação dos usuários

  • Novos usuários terão menos necessidade de aprender sobre codificação de texto durante o primeiro ano
  • Eles só precisarão aprender sobre codificação quando tiverem de lidar com arquivos de texto que não sejam UTF-8
  • Usuários existentes devem verificar os pontos afetados seguindo o procedimento de compatibilidade retroativa

1 comentários

 
GN⁺ 2024-04-28
Opiniões no Hacker News
  • Sempre foi irritante que a codificação padrão de arquivos de texto variasse conforme a plataforma, então essa mudança é bem-vinda.
    Também é bom que não tenham tentado mexer na codificação do sistema de arquivos. Isso é outro problema, e bem espinhoso por si só.

    • No Windows, a página de código padrão do sistema depende não só da plataforma, mas também da localidade do sistema.
      Foi um grande erro o Windows ter passado tanto tempo sem oferecer uma forma simples de escolher que funções ANSI como TextOutA usassem a página de código UTF-8. Isso se tornou possível via arquivo de manifest mais ou menos no meio do desenvolvimento do Windows 10, mas esse tipo de recurso deveria ter entrado na época do NT4 ou do Windows 98.
    • Historicamente, fazia sentido. A maior parte do software era apenas local, e esperava-se que arquivos de texto também estivessem na codificação local.
      Dependia não só da plataforma, mas também da localidade preferida do usuário, e a biblioteca padrão de C se comporta do mesmo jeito. Por exemplo, em Unix/Linux, iso-8859-1 era comum para línguas da Europa Ocidental e, depois da introdução do euro, tornou-se frequente mudar para iso-8859-15, que tinha o símbolo . O UTF-8 começou a funcionar sem grandes problemas lá pelo fim dos anos 2000, e o Debian mudou o padrão para UTF-8 no lançamento Etch.
    • Alguns dias atrás, fui pego por uma mudança implícita de quebras de linha.
      No notebook da empresa, todos os testes locais funcionavam bem, mas, ao implantar em um host Linux, uma aplicação subordinada exigia CRLF e não conseguiu consumir o arquivo. É um daqueles probleminhas bobos que às vezes precisamos lembrar. Dito isso, também é uma pergunta válida por que um software recém-escrito exigiria um terminador de linha específico.
    • Quando alguém começa a escrever código no Windows, acaba sendo pego por esse problema várias vezes.
  • Não depender de padrões instáveis do sistema é uma coisa boa.
    Esses valores tendem, em algum momento, a voltar diferentes do que eu presumia. Alguns anos atrás, ao lidar com Ubuntu e scripts init.d, um script que iniciava Java estava sendo executado como root — e isso era antes do Docker, então era ainda mais comum — em um shell que não configurava o padrão UTF-8 normal para usuários comuns. O resultado foi expor um uso ruim de APIs do Java que usavam o padrão do SO.
    Hoje em dia, a maioria das APIs tem variantes em que é possível especificar a codificação, e analisadores estáticos de código avisam quando você usa a errada. Mas basta faltar em um único ponto para o conteúdo começar a quebrar. Agora é muito provável que o uso de codificações que não sejam UTF-8 seja, na maioria das vezes, não intencional; se for intencional, isso deve ser explicitado, em vez de depender de alguma configuração indireta estranha do SO. Portanto, é uma boa mudança, e é melhor que o código que quebrar por causa disso receba uma correção simples.

    • Eu estava usando um .gitignore criado por uma função touch feita como alias no PowerShell, e o Git simplesmente não o respeitava de jeito nenhum.
      Ao verificar, descobri que o arquivo de texto gerado era UTF-16, então estava sendo basicamente ignorado. Aprendi a lição e mudei o padrão do sistema para UTF-8, mas hoje simplesmente dependo do editor de texto.
    • A localidade global foi um erro não só para codificação, mas de modo geral.
      Se printf("%f", 4.2) imprime magicamente strings diferentes dependendo do ambiente, isso cria mais problemas do que resolve. Quando se deseja comportamento dependente de localidade, as informações de localidade ou as partes relevantes devem ser passadas explicitamente para a função.
  • Há uma heurística que vem ficando cada vez mais correta nas últimas décadas: se existe uma configuração de charset em algum lugar e ela não é UTF-8, está errada.
    Python 2 era indiferente ao conjunto de caracteres e, por isso, sempre funcionava, mas a melhoria do Python 3 não foi simplesmente uma melhoria. A forma de distinguir um script Python 3 de um script Python 2 é esta: se contém a string utf-8, é Python 3; se só funciona na localidade C.UTF-8, é Python 3. Entendo essa mudança como um “conserto” do Python 3, então a recebo bem.

  • Achei que isso já fosse o padrão desde o Python 3.

    • Provavelmente você está pensando nas strings em que o prefixo u"" deixou de ser necessário no Python 3.
      Acabei de digitar "éķů" no Python 2.7 e ele imprimiu os bytes UTF-8 desses caracteres, então não tenho muita certeza do que exatamente o prefixo u fazia, mas uma das grandes mudanças na passagem do Python 2 para o 3 foi que strings passaram a ter codificação, enquanto strings de bytes viraram sequências de bytes sem codificação. Esta mudança parece tratar principalmente do problema de, em ambientes cuja codificação padrão não é UTF-8, como o Windows, ser necessário especificar open('filename', mode='r', encoding='UTF-8') ao usar open('filename', mode='r').
    • No Python 3, o código-fonte Python é UTF-8 por padrão. Mas isso não diz nada sobre a codificação de caracteres usada ao salvar em arquivos, e o padrão depende da localidade.
      Como em Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding"), a codificação de literais de string, nomes de arquivo e conteúdo de arquivos é diferente em cada caso. As codificações correspondentes são o UTF-8 de tokenize.open, sys.getfilesystemencoding() de os.fsencode e locale.getpreferredencoding() de open.
  • A frase “outras linguagens de programação populares, incluindo Node.js, Go, Rust e Java, também usam UTF-8 por padrão” — eu perdi quando o Java migrou de UTF-16 para UTF-8.

    • A codificação padrão ao converter bytes em strings no Java costumava depender da plataforma e agora é UTF-8.
      Internamente, a classe String ainda usa as codificações UTF-16 e latin-1, e a JVM usa, como antes, uma codificação UTF-8 modificada. A classe String originalmente usava apenas UTF-16, mas desde o Java 9 também usa a codificação latin-1 de 1 byte por caractere quando possível.
    • Parece que estão misturando representação interna de strings com codificação de leitura/escrita.
      O Java nunca usou UTF-16 como padrão para codificação de leitura/escrita.
    • Parece ter mudado há dois anos, no Java 18.
  • A codificação interna do CPython agora é UTF-8?
    Strings em Python podem ser indexadas por subscrito, mas acesso aleatório é raro o bastante para que pareça aceitável fazer uma indexação preguiçosa quando necessário. Se você só precisa avançar ou retroceder uma posição, não precisa de índice, então também é perfeitamente possível usar UTF-8 como representação interna

    • O que representa uma str é um objeto PyUnicode
      Quando bytes UTF-8 são solicitados, um objeto bytes é criado conforme necessário e armazenado em cache como parte do PyUnicode, sendo liberado junto quando o PyUnicode é liberado. Separadamente, os code points que compõem a string são armazenados em um array simples para permitir acesso aleatório. O tamanho de cada code point pode ser de 1, 2 ou 4 bytes; ao criar um PyUnicode, se você especifica o valor máximo de code point, ele é arredondado para um de 127, 255, 65535 ou 1.114.111, determinando se serão usados 1/2/4 bytes
      Se o valor máximo de code point for 127, essa representação em array pode ser usada diretamente como UTF-8. Então a resposta à pergunta é que muitas strings em que todos os code points são 127 ou menos são armazenadas como UTF-8. Porém, ao percorrer uma string, não se deve fazer isso por code point. Um caractere percebido pelo usuário, isto é, um cluster de grafemas, é composto por um ou mais code points. Por exemplo, um e com acento pode ser o code point e seguido de um code point de acento combinante, e o emoji de fênix é composto pelo emoji de pássaro, um conector de largura zero e o emoji de fogo. Alguns sistemas de escrita usados por centenas de milhões de pessoas também funcionam de modo parecido, com sinais combinantes que indicam vogais anexados a consoantes. Este - - tem 5 code points, e há um bom texto sobre como várias linguagens reportam seu “comprimento”: https://hsivonen.fi/string-length/. Isso vem da experiência de ter acabado de implementar, como extensão C para Python, a Unicode TR29 que trata dessa parte
  • Fico me perguntando por que não utf-8-sig. Ele lida com o BOM opcional, e ainda na semana passada tive que corrigir um script por causa disso

    • Hoje em dia, nada deveria colocar BOM em UTF-8
      Isso nem é recomendado, e hoje considero razoável também o comportamento de falhar ao encontrar um BOM
    • Não é uma boa ideia mudar o Python para adicionar silenciosamente um BOM invisível antes de toda entrada e saída
  • Já que o assunto é UTF-8, o framebuffer do Linux já deveria ter ganhado suporte UTF-8 de verdade há muito tempo
    Quero dizer suporte real, não de 256/512 glifos. Até o GNU Hurd já tinha, por volta de 2007, um console de terminal melhor com suporte a UTF-8, e agora estamos em 2024

  • Ótimo. Agora só falta o JS mudar para UTF-8
    Claro que o JS não pode ser melhorado. Porque, ao contrário de qualquer outra linguagem de programação, ele precisa ser compatível com código escrito em 1995

    • Aqui a questão é qual codificação usar por padrão quando se pede ao Python para abrir um arquivo “como texto”
      A representação interna das strings é uma questão separada e, assim como JavaScript, Python também não usa “simplesmente UTF-8” internamente
  • Em “muitos desenvolvedores Python que usam Unix esquecem que a codificação padrão depende da plataforma e omitem encoding="utf-8" ao ler arquivos de texto codificados em UTF-8”, talvez não seja tanto que tenham esquecido, mas sim que isso não seja suficientemente conhecido
    Sinceramente, eu achava que o Python usava apenas UTF-8 em todos os lugares, a menos que você pedisse explicitamente algo diferente

    • Na prática, depende do caso
      bytes.decode e str.encode usam UTF-8 como padrão pelo menos desde o Python 3. Já a codificação padrão ao decodificar nomes de arquivo usa sys.getfilesystemencoding(); no Windows e no macOS isso também é UTF-8, mas no Linux depende do locale, mais especificamente de CODESET. Por fim, open usa diretamente locale.getencoding()