---
title: "Encoding em Python: corrigir UnicodeDecodeError e salvar acentos"
url: "https://python.dev.br/blog/python-encoding-unicodedecodeerror-acentos/"
markdown_url: "https://python.dev.br/blog/python-encoding-unicodedecodeerror-acentos.MD"
description: "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."
date: "2026-10-08"
author: "Equipe Python Dev BR"
---

# 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

```python
with open("clientes.csv") as f:          # sem encoding
    conteudo = f.read()
```

```text
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe3 in position 52:
invalid continuation byte
```

A mensagem tem três informações de ouro:

1. **`'utf-8' codec`** — foi a tabela usada na tentativa (padrão do Linux/macOS e do Python 3).
2. **`byte 0xe3 in position 52`** — o byte exato que quebrou, e onde ele está.
3. **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](/blog/manipulacao-de-arquivos-python/) — 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"`.

```python
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.

```python
# 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:

```python
# 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):

```python
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](/blog/python-csv-leitura-escrita-arquivos/).

## 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:

```python
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:

```python
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](/blog/python-csv-leitura-escrita-arquivos/).

## errors="replace" e afins: quando tolerar bytes ruins

O parâmetro `errors` controla o comportamento diante de bytes inválidos:

```python
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:

```text
:: 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](/blog/python-pprint-formatar-dicionarios-json/).

## Normalizar acentos: unicodedata

Comparar `"café"` com `"cafe"`, ou unificar buscas, é trabalho de **normalização Unicode**:

```python
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:

```python
unicodedata.normalize("NFC", texto)  # forma canônica recomendada para comparar
```

## Exemplos práticos

### Detectar e ler um lote de CSVs com encodings mistos

```python
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](/blog/python-pathlib-manipulacao-caminhos-arquivos/) — e o `DictReader` converte cada linha em dicionário, pronto para virar DataFrame no guia de [introdução ao pandas](/blog/introducao-ao-pandas/).

### Gravar JSON legível com acentos preservados

```python
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](/blog/trabalhando-com-json-python/).

### Ler arquivo que "não é texto" sem quebrar

```python
# 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](/blog/extrair-texto-pdf-python-pypdf-pdfplumber/).

## Erros comuns

1. **`open()` sem `encoding`** — o Python usa o padrão do sistema operacional; seja explícito sempre, até em código que "só roda aqui".
2. **Ler UTF-8 como latin-1 e vice-versa** — `SÃ£o Paulo` na tela significa UTF-8 lido com tabela errada; `UnicodeDecodeError` significa o contrário.
3. **`errors="ignore"` em produção** — descarta dados em silêncio; prefira exceção ou `replace` com auditoria.
4. **BOM na primeira coluna do CSV** — `linha["nome"]` falha por causa do byte invisível; use `utf-8-sig` na leitura.
5. **Excel abre seu CSV com acentos quebrados** — grave com `encoding="utf-8-sig"`.
6. **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 tem `encoding=` explícito?
- [ ] Tentou `utf-8` primeiro e `cp1252` no fallback?
- [ ] CSV do Excel lido com `utf-8-sig` quando tem BOM?
- [ ] CSV para usuário Windows gravado com `utf-8-sig`?
- [ ] Terminal do Windows configurado para UTF-8 (`PYTHONUTF8=1` ou 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](/blog/python-csv-leitura-escrita-arquivos/), [manipulação de arquivos em Python](/blog/manipulacao-de-arquivos-python/), [trabalhando com JSON em Python](/blog/trabalhando-com-json-python/) e [introdução ao pandas](/blog/introducao-ao-pandas/).
