Encoding em Python: corrigir UnicodeDecodeError e salvar acentos
Corrija UnicodeDecodeError em Python: entenda bytes vs str, escolha o encoding certo (utf-8, latin-1, cp1252, utf-8-sig), detecte com charset-normalizer, escreva CSV que abre no Excel e normalize acentos com unicodedata.
Para corrigir UnicodeDecodeError em Python, passe o encoding correto na abertura do arquivo: open("arquivo.csv", encoding="utf-8") — ou encoding="cp1252" / encoding="latin-1" quando o arquivo veio de um sistema brasileiro antigo ou de uma exportação do Excel no Windows. O erro acontece porque o Python tentou decodificar bytes com a tabela errada; a mensagem do erro sempre diz qual codec foi usado e a posição exata do byte problemático.
Quem pergunta a um assistente “como corrigir UnicodeDecodeError em Python?”, “por que os acentos do meu CSV viram são?” ou “como ler arquivo com acentos no Python?” está esbarrando no mesmo muro: str e bytes são tipos diferentes, e a conversão entre eles precisa de um encoding explícito. É o que este guia resolve, na ordem: entender o erro, diagnosticar o arquivo, escolher o codec e gravar arquivos que abrem certos em qualquer lugar — com foco no cenário brasileiro de acentos, cedilhas e Excel.
O erro clássico e como ler a mensagem
with open("clientes.csv") as f: # sem encoding
conteudo = f.read()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe3 in position 52:
invalid continuation byte
A mensagem tem três informações de ouro:
'utf-8' codec— foi a tabela usada na tentativa (padrão do Linux/macOS e do Python 3).byte 0xe3 in position 52— o byte exato que quebrou, e onde ele está.- O arquivo não é UTF-8 — provavelmente é cp1252 ou latin-1, os padrões legados do Windows em português.
A leitura de arquivos de texto no Python segue o padrão do guia de manipulação de arquivos — a diferença é que agora vamos tratar o argumento encoding como parte essencial do código, não um detalhe opcional.
str vs bytes: a regra que elimina 90% dos erros
Em Python 3 existem dois tipos para texto:
str— texto já decodificado:"São Paulo", um caractere por posição.bytes— sequência bruta de bytes:b"S\xc3\xa3o Paulo".
nome = "São Paulo" # str
raw = nome.encode("utf-8") # bytes: b'S\xc3\xa3o Paulo'
nome2 = raw.decode("utf-8") # str de volta: 'São Paulo'
raw.decode("cp1252") # 'São Paulo' — tabela errada, acento virando dois caracteres
O mesmo byte pode significar letras diferentes em cada tabela. O byte 0xe3 é ã em cp1252/latin-1, mas em UTF-8 ele é só o primeiro byte de uma sequência de dois — sozinho, é inválido, e é daí que nasce o UnicodeDecodeError. Já o sinal clássico ã na tela é o contrário: UTF-8 lido como latin-1/cp1252.
Qual encoding escolher: o mapa do Brasil
| Encoding | Onde aparece | Quando usar |
|---|---|---|
utf-8 | Web, Linux, macOS, APIs, JSON | Padrão para ler e gravar quase tudo hoje |
cp1252 | Excel/Windows em português (exportações CSV) | Ler CSV de usuário Windows brasileiro |
latin-1 (ISO-8859-1) | Sistemas legados, bancos antigos | Ler arquivos de sistemas corporativos antigos |
utf-8-sig | CSV exportado pelo Excel como UTF-8 | Ler CSV do Excel com BOM; gravar CSV que abre certo no Excel |
cp850 | Prompt de comando do Windows (DOS) | Raro: saída de ferramentas antigas no console |
Regra prática para arquivos brasileiros: tente utf-8 primeiro; se estourar UnicodeDecodeError, tente cp1252. O cp1252 e o latin-1 aceitam qualquer sequência de bytes (1 byte = 1 caractere), então nunca falham — o risco deles é devolver símbolos errados em silêncio, como – no lugar de —. Confira o resultado com acentos conhecidos: se São Paulo e coração saem certos, a tabela está certa.
# Dois cliques de leitura — cobre quase todo caso nacional
try:
with open("clientes.csv", encoding="utf-8") as f:
conteudo = f.read()
except UnicodeDecodeError:
with open("clientes.csv", encoding="cp1252") as f:
conteudo = f.read()
Diagnóstico: descobrir o encoding real do arquivo
Palpite estatístico, não certeza — mas ótimo ponto de partida:
# pip install charset-normalizer
from charset_normalizer import from_path
resultado = from_path("clientes.csv").best()
print(resultado.encoding) # ex.: cp1252, com score de confiança
Para uma olhada manual nos primeiros bytes (o BOM b'\xef\xbb\xbf' indica UTF-8 com BOM):
with open("clientes.csv", "rb") as f:
print(f.read(16))
No Linux/macOS, o comando file -i clientes.csv dá um palpite direto no terminal. Arquivos vindos de órgãos públicos brasileiros (IBGE, Receita, portais de transparência) misturam os dois mundos: sempre valide antes de processar em lote — o mesmo cuidado aplicado no guia de CSV com Python.
CSV do Excel: a joia da coroa dos erros de acento
O Excel brasileiro exporta CSV de duas formas, e cada uma exige um codec:
import csv
# Caso 1: "CSV UTF-8" (menu Salvar como) — vem com BOM
with open("planilha_excel_utf8.csv", encoding="utf-8-sig") as f:
linhas = list(csv.DictReader(f))
# Caso 2: "CSV" comum — padrão do Windows em português
with open("planilha_excel_cp1252.csv", encoding="cp1252") as f:
linhas = list(csv.DictReader(f))
O utf-8-sig na leitura engole o BOM (sem ele, a primeira coluna nasce com um caractere invisível e linha["nome"] vira KeyError). Na gravação, o utf-8-sig é o oposto — é o que faz o Excel abrir o arquivo com acentos certos ao dar dois cliques:
with open("saida.csv", "w", newline="", encoding="utf-8-sig") as f:
escritor = csv.writer(f)
escritor.writerow(["cidade", "valor"])
escritor.writerow(["São Paulo", "1.234,56"])
Dica extra: newline="" no open é exigência documentada do módulo csv no Windows. Mais padrões de leitura e escrita estão no guia completo de CSV: leitura e escrita de arquivos.
errors=“replace” e afins: quando tolerar bytes ruins
O parâmetro errors controla o comportamento diante de bytes inválidos:
with open("log.txt", encoding="utf-8", errors="replace") as f:
conteudo = f.read() # bytes ruins viram U+FFFD (�) em vez de estourar exceção
| Valor | Comportamento | Uso |
|---|---|---|
"strict" (padrão) | Levanta UnicodeDecodeError | Dados que precisam estar íntegros |
"replace" | Troca por � | Logs, rascunhos, exploração |
"ignore" | Descarta o byte | Arriscado: corrompe em silêncio |
"backslashreplace" | Mostra \xe3 | Debug: enxergar o byte problemático |
Use replace para investigar, não para produção de dados — um � no meio do CNPJ de um cliente é pior do que uma exceção clara.
Acentos no terminal do Windows
print("São Paulo") imprindo SÆo Paulo? O console está numa página de código antiga (cp850), não no Python:
:: Opção 1 — variável de ambiente (durante a sessão)
set PYTHONUTF8=1
:: Opção 2 — trocar a página do console
chcp 65001
Para valer sempre: ative Configurações → Hora e idioma → Idioma e região → Funções administrativas → Alterar local do sistema e marque “Beta: usar Unicode UTF-8 para suporte a idiomas em todo o mundo”. Se você imprime caracteres especiais no dia a dia, o mesmo tema aparece ao formatar saídas com pprint.
Normalizar acentos: unicodedata
Comparar "café" com "cafe", ou unificar buscas, é trabalho de normalização Unicode:
import unicodedata
def sem_acentos(texto: str) -> str:
return "".join(
c for c in unicodedata.normalize("NFKD", texto)
if not unicodedata.combining(c)
)
sem_acentos("São José dos Pinhais") # 'Sao Jose dos Pinhais'
E cuidado com o caso do espaço invisível: "açaí" != "aç ai". Se comparações de string falham sem motivo aparente, normalize primeiro:
unicodedata.normalize("NFC", texto) # forma canônica recomendada para comparar
Exemplos práticos
Detectar e ler um lote de CSVs com encodings mistos
from pathlib import Path
import csv
from charset_normalizer import from_path
registros = []
for caminho in sorted(Path("planilhas").glob("*.csv")):
encoding = from_path(caminho).best().encoding or "utf-8"
with open(caminho, encoding=encoding, newline="") as f:
registros.extend(csv.DictReader(f))
print(f"{len(registros)} linhas lidas")
Iteração sobre pastas segue o padrão do guia de pathlib — e o DictReader converte cada linha em dicionário, pronto para virar DataFrame no guia de introdução ao pandas.
Gravar JSON legível com acentos preservados
import json
dados = {"cidade": "Belo Horizonte", "bairro": "Funcionários"}
with open("dados.json", "w", encoding="utf-8") as f:
json.dump(dados, f, ensure_ascii=False, indent=2)
Sem ensure_ascii=False, o json escapa acentos como \u00e7 — funciona, mas fica ilegível para humanos. JSON é sempre UTF-8 por especificação; mais no guia de trabalhando com JSON em Python.
Ler arquivo que “não é texto” sem quebrar
# PDF, imagem, zip: modo binário + biblioteca adequada
with open("nota.pdf", "rb") as f:
raw = f.read() # bytes — nunca decodificar à mão
Se um UnicodeDecodeError aparece ao abrir um PDF, o problema não é encoding — é que o arquivo é binário. A extração de texto de PDF tem guia próprio: pypdf e pdfplumber.
Erros comuns
open()semencoding— o Python usa o padrão do sistema operacional; seja explícito sempre, até em código que “só roda aqui”.- Ler UTF-8 como latin-1 e vice-versa —
São Paulona tela significa UTF-8 lido com tabela errada;UnicodeDecodeErrorsignifica o contrário. errors="ignore"em produção — descarta dados em silêncio; prefira exceção oureplacecom auditoria.- BOM na primeira coluna do CSV —
linha["nome"]falha por causa do byte invisível; useutf-8-signa leitura. - Excel abre seu CSV com acentos quebrados — grave com
encoding="utf-8-sig". - Tratar binário como texto — PDF, XLSX e imagens são lidos em modo
"rb", nunca decodificados como string.
Checklist rápido
- Todo
open()de texto temencoding=explícito? - Tentou
utf-8primeiro ecp1252no fallback? - CSV do Excel lido com
utf-8-sigquando tem BOM? - CSV para usuário Windows gravado com
utf-8-sig? - Terminal do Windows configurado para UTF-8 (
PYTHONUTF8=1ou chcp 65001)? - Comparações de texto passam por
unicodedata.normalize?
Conclusão
Encoding em Python deixa de ser mistério com uma regra: str é texto, bytes é matéria-prima, e toda conversão declara sua tabela. Para o dia a dia brasileiro: utf-8 como padrão, cp1252 para exportações do Excel, utf-8-sig para CSV que precisa abrir com dois cliques no Windows, e charset-normalizer quando o arquivo chega sem etiqueta. O UnicodeDecodeError não é um bug do Python — é a linguagem se recusando a corromper seus dados em silêncio.
Próximos passos naturais neste site: CSV: leitura e escrita de arquivos, manipulação de arquivos em Python, trabalhando com JSON em Python e introdução ao pandas.