Em Python, texto é str e dados binários são bytes. Essa é a resposta inicial que resolve a maior parte das dúvidas: use str enquanto manipula texto dentro do programa, converta para bytes com encode() ao atravessar uma fronteira binária e recupere str com decode() ao entrar novamente no domínio textual. UTF-8 é a codificação normalmente escolhida para essa conversão, não um tipo de string.
Essa separação aparece ao abrir arquivos, transmitir HTTP, conversar com bancos e processar JSON. Misturar os dois domínios produz erros explícitos, como UnicodeDecodeError, ou defeitos silenciosos, como João. Entender o modelo evita tentativas aleatórias de “consertar acentos” e torna o diagnóstico previsível.
Unicode não é UTF-8
Unicode é um repertório que atribui um ponto de código a caracteres. A letra A é U+0041; ç é U+00E7. Uma str representa uma sequência desses caracteres, independentemente de como será gravada. Para revisar operações textuais, veja também o guia de strings em Python.
UTF-8 define como pontos de código viram octetos. Caracteres ASCII usam um byte; muitos caracteres latinos usam dois; outros podem usar três ou quatro. Por isso, quantidade de caracteres e tamanho binário não são sinônimos:
texto = "ação"
dados = texto.encode("utf-8")
print(len(texto)) # 4 caracteres
print(len(dados)) # 6 bytes
print(dados) # b'a\xc3\xa7\xc3\xa3o'
print(dados.decode("utf-8") == texto) # True
O prefixo b identifica um literal de bytes. Escapes como \xc3 mostram valores hexadecimais, não texto corrompido. A documentação do Unicode HOWTO aprofunda pontos de código, representações e normalização.
Encode na saída, decode na entrada
encode() pertence a str e devolve bytes; decode() pertence a bytes e devolve str. Uma regra útil é decodificar uma vez, logo após receber bytes, e codificar uma vez, imediatamente antes de enviá-los. O núcleo da aplicação permanece textual.
def montar_mensagem(nome: str) -> bytes:
texto = f"Olá, {nome}!"
return texto.encode("utf-8")
pacote = montar_mensagem("Lívia")
mensagem = pacote.decode("utf-8")
print(mensagem)
Não aplique str(dados) para decodificar. str(b"olá") requer até que o literal seja válido e, em bytes reais, produz uma representação como "b'ol\xc3\xa1'". Use a codificação declarada pelo protocolo. A referência oficial de bytes e bytearray detalha operações binárias e mutabilidade.
Arquivos e JSON
Em modo texto, open realiza as conversões. Declare encoding="utf-8" para que o resultado não dependa da configuração da máquina:
from pathlib import Path
caminho = Path("clientes.txt")
caminho.write_text("Ana\nJosé\n", encoding="utf-8")
conteudo = caminho.read_text(encoding="utf-8")
print(conteudo.splitlines())
O mesmo princípio vale para open(caminho, "w", encoding="utf-8", newline=""). O tutorial de manipulação de arquivos TXT, CSV e JSON mostra formatos completos, enquanto pathlib em Python simplifica caminhos e leitura textual.
JSON é um formato textual. json.dumps() retorna str; json.loads() normalmente recebe str. Para preservar caracteres legíveis, use ensure_ascii=False; depois codifique se uma biblioteca exigir bytes:
import json
registro = {"cidade": "São Luís", "moeda": "€"}
texto_json = json.dumps(registro, ensure_ascii=False)
corpo_http = texto_json.encode("utf-8")
restaurado = json.loads(corpo_http.decode("utf-8"))
assert restaurado == registro
Consulte JSON em Python para validação e serialização de estruturas maiores.
Erros de codificação sem adivinhação
UnicodeEncodeError significa que a codificação de destino não representa algum caractere. ASCII, por exemplo, não representa ã. UnicodeDecodeError significa que a sequência de bytes não é válida na codificação escolhida. O erro registra codec, posição e motivo:
dados_latin1 = b"Ol\xe1"
try:
print(dados_latin1.decode("utf-8"))
except UnicodeDecodeError as erro:
print(erro.encoding, erro.start, erro.reason)
print(dados_latin1.decode("latin-1")) # Olá
A solução correta é descobrir a origem: cabeçalho HTTP, especificação do arquivo, contrato do fornecedor ou configuração do banco. Detector de encoding fornece hipótese, nunca prova. Se você controla o sistema, padronize UTF-8 de ponta a ponta.
Os manipuladores errors="ignore" e errors="replace" evitam exceções, mas descartam informação. Isso é perigoso em nomes, identificadores, assinaturas e auditoria. Prefira errors="strict", o padrão. Para uma importação legada em que perda foi aceita conscientemente, replace insere � e permite contabilizar registros afetados. backslashreplace é útil em logs porque conserva uma representação do valor problemático. A lista completa está na documentação de codecs do Python.
Mojibake e dupla codificação
João costuma surgir quando bytes UTF-8 foram decodificados como Latin-1. O texto errado pode então ser salvo, perpetuando o dano. Não corrija com substituições manuais. Reprocesse os bytes originais com o codec correto. Se só restou a string corrompida e a cadeia é conhecida, uma recuperação controlada pode ser testada:
corrompido = "João"
recuperado = corrompido.encode("latin-1").decode("utf-8")
print(recuperado) # João
Isso não é receita universal. Faça backup, valide amostras e registre alterações. Aplicar a transformação duas vezes corrompe dados válidos.
Normalização e comparação segura
Visualmente, é pode ser um ponto de código ou e seguido de acento combinante. As strings parecem iguais, mas podem ter bytes e comparação diferentes. Normalize antes de comparar entradas vindas de fontes distintas:
import unicodedata
a = "café"
b = "cafe\u0301"
print(a == b) # False
print(unicodedata.normalize("NFC", a) == unicodedata.normalize("NFC", b)) # True
NFC é uma escolha comum para armazenamento. NFKC também aplica equivalências de compatibilidade e pode alterar significado; não a use sem requisito. Para busca sem diferença entre maiúsculas e minúsculas, casefold() é mais abrangente que lower(), mas autorização nunca deve depender apenas de aparência textual.
Práticas, segurança e observabilidade
Defina encoding em arquivos e contratos. Não registre segredos nem corpos completos só para investigar um erro; registre origem, codec declarado, posição e uma amostra limitada em hexadecimal. Limite o tamanho antes de decodificar dados externos, pois uma entrada enorme ainda consome memória. Valide conteúdo depois da decodificação: UTF-8 válido não significa dado confiável.
Em URLs e HTTP, deixe bibliotecas maduras tratarem percent-encoding e cabeçalhos. Em bancos, confirme conexão, tabela e coluna em Unicode. Teste nomes com acentos, caracteres combinantes, alfabetos não latinos e emoji, mesmo que a interface não os destaque. O guia de tratamento de erros com try/except ajuda a capturar falhas no limite certo; logging em Python mostra como preservar contexto sem esconder exceções.
Perguntas frequentes
Qual é a diferença entre str e bytes?
str contém texto Unicode; bytes contém números de 0 a 255. Codificação transforma texto em bytes, e decodificação faz o caminho inverso.
Devo sempre usar UTF-8?
Use UTF-8 por padrão quando controla o contrato. Ao consumir dados externos, respeite a codificação declarada e não imponha UTF-8 a bytes legados.
Posso usar errors="ignore" para eliminar o erro?
Tecnicamente sim, mas ele remove dados silenciosamente. Use apenas quando a perda for explicitamente aceitável e mensurada; para dados de negócio, prefira falhar e corrigir a origem.
Por que o texto aparece como b'...'?
Porque o valor ainda é bytes ou sua representação foi convertida com str(). Decodifique os bytes uma única vez usando o codec correto.
Conclusão
Unicode modela caracteres, UTF-8 os representa em bytes, encode() prepara texto para uma fronteira binária e decode() interpreta bytes recebidos. Mantendo str no núcleo, declarando codecs nas bordas e tratando erros sem descartar informação, arquivos e integrações deixam de depender de sorte. Quando algo falhar, preserve os bytes originais, identifique o contrato e corrija a fronteira, não os sintomas.