Python JSON

A agenda de contatos da Ana é uma lista de dicionários: cada contato tem nome, telefones, endereço. Enquanto o programa está rodando, tudo bem. Mas quando ele termina, as variáveis somem, e com elas a agenda. Para guardar essas estruturas num arquivo e recuperá-las depois, do jeitinho que estavam, o formato mais usado no mundo é o JSON. É também a língua em que os sites e aplicativos trocam dados pela internet. E o Python fala JSON com o módulo json, que já vem instalado.

Você vai aprender
  • O que é JSON e como ele se parece (e não se parece) com Python.
  • A transformar um dicionário em texto JSON com json.dumps, e o texto de volta em dicionário com json.loads.
  • A deixar os acentos legíveis com ensure_ascii=False e o texto arrumado com indent.
  • A gravar e ler arquivos .json com json.dump e json.load.
  • A tabela de tradução Python ↔ JSON, e o que muda na ida e na volta.
  • Os erros reais: JSONDecodeError, o set que não vira JSON e o dump sem arquivo.

A ideia: uma ficha que qualquer programa entende

Imagine que a Ana precisa passar a ficha de um produto para outra loja, que usa outro sistema. Se ela mandar "o dicionário do Python", o outro sistema (que pode nem ser em Python) não vai entender. Os dois precisam combinar um formato de texto, com regras fixas, que qualquer linguagem saiba escrever e ler. Esse formato é o JSON (JavaScript Object Notation, "notação de objetos do JavaScript"; o nome vem da linguagem em que ele nasceu, mas hoje é de todo mundo).

Um arquivo JSON é assim:

{
    "nome": "café",
    "preco": 18.9,
    "estoque": 12,
    "promocao": true,
    "fornecedor": null
}

Parece um dicionário do Python, e quase é. As diferenças são poucas, mas são rígidas:

No PythonNo JSON
Textos com aspas simples ou duplas: 'café' ou "café"Só aspas duplas: "café"
True, Falsetrue, false (minúsculas)
Nonenull
Chaves de qualquer tipo imutável: {1: "a"}Chaves só de texto: {"1": "a"}
Vírgula sobrando no fim é permitida: [1, 2, ]Vírgula no fim é erro
Comentários com #Não existem comentários

O módulo json faz a tradução nos dois sentidos, e você quase nunca precisa escrever JSON à mão.


1. Do Python para o JSON: json.dumps

import json

produto = {"nome": "café", "preco": 18.9, "estoque": 12, "promocao": True, "fornecedor": None}

texto = json.dumps(produto)
print(texto)
print(type(texto))
$ python3 dumps1.py
{"nome": "café", "preco": 18.9, "estoque": 12, "promocao": true, "fornecedor": null}
<class 'str'>
import json
Traz o módulo json.
json.dumps(produto)
Transforma o dicionário num texto JSON. O nome vem de dump string: "despejar numa string". O s do fim é de string, e vai ajudar a lembrar a diferença para o dump da seção 4.
type(texto)
str: o resultado é um texto comum, que dá para gravar, mandar pela internet, mostrar.

Olhe a tradução: True virou true, None virou null, e todas as aspas são duplas. Mas o que aconteceu com o "café"? Virou café.

Os acentos: ensure_ascii=False

O é é o código da letra é, escrito só com letras e números simples. Por padrão, o dumps faz isso com toda letra que não é do alfabeto inglês básico (o ASCII), por garantia (ensure ASCII, "garantir ASCII"). Não está errado: qualquer programa que ler esse JSON vai entender "café". Mas, para um ser humano, fica ilegível. A opção ensure_ascii=False desliga isso:

import json

produto = {"nome": "café", "preco": 18.9, "estoque": 12, "promocao": True, "fornecedor": None}

print(json.dumps(produto, ensure_ascii=False))
$ python3 dumps2.py
{"nome": "café", "preco": 18.9, "estoque": 12, "promocao": true, "fornecedor": null}

Agora sim, "café". Em português, com tanto acento, use sempre ensure_ascii=False (e, ao gravar num arquivo, encoding="utf-8").

Missão: rode e veja os códigos ã, ç… no lugar dos acentos. Acrescente a opção que faz o JSON mostrar "pão de queijo" e "açúcar" do jeito que se escreve.

import json

lista = {"padaria": ["pão de queijo", "açúcar", "café"]}
print(json.dumps(lista))
import json

lista = {"padaria": ["pão de queijo", "açúcar", "café"]}
print(json.dumps(lista, ensure_ascii=False))

Arrumado para ler: indent

Numa linha só, um JSON grande vira uma sopa de letras. A opção indent ("recuo") quebra as linhas e recua cada nível com o número de espaços que você escolher:

import json

contato = {
    "nome": "Bia Costa",
    "telefones": ["86 99999-0001", "86 3222-0002"],
    "endereco": {"rua": "Rua das Flores", "numero": 12},
    "aniversario": "12/03",
}

print(json.dumps(contato, ensure_ascii=False, indent=4))
$ python3 indent.py
{
    "nome": "Bia Costa",
    "telefones": [
        "86 99999-0001",
        "86 3222-0002"
    ],
    "endereco": {
        "rua": "Rua das Flores",
        "numero": 12
    },
    "aniversario": "12/03"
}

A lista de telefones e o dicionário do endereço ficaram cada um no seu nível, como as pastas no tree. Para guardar em arquivo que pessoas vão abrir, use indent=2 ou indent=4. Para mandar pela internet, onde ninguém lê e cada byte conta, deixe sem.


2. Do JSON para o Python: json.loads

O caminho de volta. Chegou um texto JSON (de um arquivo, de um site) e você quer um dicionário para trabalhar:

import json

texto = '{"nome": "Caio", "idade": 31, "cliente_vip": false, "apelido": null}'

dados = json.loads(texto)
print(dados)
print(type(dados))
print(dados["nome"], "tem", dados["idade"], "anos")
$ python3 loads1.py
{'nome': 'Caio', 'idade': 31, 'cliente_vip': False, 'apelido': None}
<class 'dict'>
Caio tem 31 anos
texto = '{"nome": …}'
Um texto Python (entre aspas simples) que guarda um JSON (com aspas duplas por dentro). As aspas simples de fora são do Python; as duplas de dentro são do JSON.
json.loads(texto)
Load string, "carregar de uma string": lê o texto JSON e monta o valor Python equivalente.
dados["nome"]
Agora é um dicionário de verdade: as chaves funcionam como em qualquer dicionário.

Repare na tradução ao contrário: false virou False e null virou None. E o print do dicionário mostra aspas simples, porque é assim que o Python mostra os seus textos.


3. A tabela de tradução

Pythonvira em JSONe volta para o Python como
dictobjeto {…}dict
listarray (lista) […]list
tuplearray […]list (a tupla não volta!)
strtexto "…"str
int, floatnúmeroint, float
True / Falsetrue / falseTrue / False
NonenullNone
chave int ({1: …})chave texto {"1": …}chave str ({'1': …})
set, datas, objetos seusNão têm tradução: dá TypeError (veja em Quando dá errado)

A ida e volta nem sempre devolve exatamente o que foi:

import json

original = {"notas": (8.5, 7.0), 1: "primeiro", "ativo": True}
texto = json.dumps(original)
print(texto)

de_volta = json.loads(texto)
print(de_volta)
print(original == de_volta)
$ python3 ida_volta.py
{"notas": [8.5, 7.0], "1": "primeiro", "ativo": true}
{'notas': [8.5, 7.0], '1': 'primeiro', 'ativo': True}
False

A tupla (8.5, 7.0) voltou como lista [8.5, 7.0], e a chave 1 (número) voltou como '1' (texto). Por isso o == deu False. O JSON não tem tupla nem chave numérica, então não tem como lembrar. Na prática: em dados que vão para JSON, use listas e chaves de texto desde o começo.


4. Com arquivos: json.dump e json.load

Agora a agenda da Ana. Para gravar num arquivo, a dupla é a mesma, sem o s: dump grava direto no arquivo aberto, load lê direto do arquivo aberto.

import json

contatos = [
    {"nome": "Bia", "telefone": "86 99999-0001"},
    {"nome": "Caio", "telefone": "86 99999-0002"},
]

with open("contatos.json", "w", encoding="utf-8") as arquivo:
    json.dump(contatos, arquivo, ensure_ascii=False, indent=2)

print("Gravei", len(contatos), "contatos")
$ python3 gravar.py
Gravei 2 contatos
open("contatos.json", "w", encoding="utf-8")
Abre (ou cria) o arquivo para escrita, com UTF-8 para os acentos. A extensão costumeira é .json.
json.dump(contatos, arquivo, …)
"Despeje contatos neste arquivo." Dois argumentos obrigatórios: o que gravar e onde. As opções são as mesmas do dumps.

O arquivo gravado, visto no terminal:

$ cat contatos.json
[
  {
    "nome": "Bia",
    "telefone": "86 99999-0001"
  },
  {
    "nome": "Caio",
    "telefone": "86 99999-0002"
  }
]

(O json.dump não põe uma quebra de linha depois do ] final. Não faz diferença para quem lê o arquivo; só que, no terminal, o próximo $ aparece colado no ].)

Outro dia, outro programa, e a agenda volta inteira:

import json

with open("contatos.json", encoding="utf-8") as arquivo:
    contatos = json.load(arquivo)

for contato in contatos:
    print(contato["nome"], "-", contato["telefone"])
$ python3 ler.py
Bia - 86 99999-0001
Caio - 86 99999-0002

json.load(arquivo) leu o arquivo todo e devolveu a lista de dicionários, pronta para o for.

FunçãoDeParaLembrete
json.dumps(valor)Pythontexto JSONs de string
json.loads(texto)texto JSONPythons de string
json.dump(valor, arquivo)Pythonarquivosem s: precisa do arquivo aberto em "w"
json.load(arquivo)arquivoPythonsem s: precisa do arquivo aberto
Conferindo um JSON pelo terminal

O Python tem um verificador de JSON pronto, que você chama com python3 -m json.tool. Ele lê o arquivo, reclama se houver erro e, se estiver tudo certo, mostra arrumado (com 4 espaços):

$ python3 -m json.tool contatos.json
[
    {
        "nome": "Bia",
        "telefone": "86 99999-0001"
    },
    {
        "nome": "Caio",
        "telefone": "86 99999-0002"
    }
]

Formas diferentes de gravar e ler

Você já conhece o pathlib (aula Pastas e Caminhos), e ele combina bem com o dumps/loads:

import json
from pathlib import Path

arquivo = Path("contatos.json")

contatos = json.loads(arquivo.read_text(encoding="utf-8"))
contatos.append({"nome": "Duda", "telefone": "86 99999-0003"})
arquivo.write_text(json.dumps(contatos, ensure_ascii=False, indent=2), encoding="utf-8")

print(len(contatos), "contatos na agenda")
$ python3 com_path.py
3 contatos na agenda
FormaQuando usar
with open(…) as f: + json.dump / json.loadO jeito clássico, o que você mais vai ver
json.loads(p.read_text(…)) / p.write_text(json.dumps(…), …)Mais curto, quando você já usa Path
json.dumps / json.loads sem arquivoQuando o JSON não está em arquivo: veio da internet, vai para um banco, vai ser mostrado
str(dicionario)Nunca como JSON: veja abaixo

str() não é JSON

Um erro clássico é achar que basta transformar o dicionário em texto:

import json

produto = {"nome": "café", "promocao": True}
print(str(produto))
print(json.dumps(produto, ensure_ascii=False))
$ python3 str_dict.py
{'nome': 'café', 'promocao': True}
{"nome": "café", "promocao": true}

Parecem iguais, mas a primeira linha tem aspas simples e True com maiúscula: é o jeito do Python mostrar o dicionário, e não JSON. Gravar isso num arquivo e tentar ler com json.load dá o primeiro erro da próxima seção.


Quando dá errado

Aspas simples: JSONDecodeError
import json

texto = "{'nome': 'café', 'promocao': True}"
dados = json.loads(texto)
$ python3 aspas_simples.py
Traceback (most recent call last):
  File "/home/ana/aspas_simples.py", line 4, in <module>
    dados = json.loads(texto)
  File "/usr/lib/python3.14/json/__init__.py", line 352, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.14/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/json/decoder.py", line 361, in raw_decode
    obj, end = self.scan_once(s, idx)
               ~~~~~~~~~~~~~~^^^^^^^^
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Vamos por partes:

  • File "/home/ana/aspas_simples.py", line 4: o erro começou no seu json.loads, na linha 4.
  • As três partes seguintes (json/__init__.py, json/decoder.py) são o caminho por dentro do módulo json. Pode pular.
  • json.decoder.JSONDecodeError: "erro de decodificação de JSON", o erro de "esse texto não é um JSON válido". O nome completo diz onde ele mora; no seu código, ele se chama json.JSONDecodeError.
  • Expecting property name enclosed in double quotes: "esperava um nome de propriedade (uma chave) entre aspas duplas".
  • line 1 column 2 (char 1): onde o leitor se perdeu: linha 1, coluna 2 (o caractere de número 1, contando do zero). Logo depois do {, ele achou ' em vez de ".

Como consertar: no JSON, só aspas duplas (e true/false/null em minúsculas). Se o texto veio de um str(dicionario), troque por json.dumps(dicionario) na hora de gravar.

Vírgula sobrando no fim
import json

texto = '{"nome": "café", "estoque": 12,}'
dados = json.loads(texto)
$ python3 virgula.py
Traceback (most recent call last):
  File "/home/ana/virgula.py", line 4, in <module>
    dados = json.loads(texto)
  File "/usr/lib/python3.14/json/__init__.py", line 352, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.14/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/json/decoder.py", line 361, in raw_decode
    obj, end = self.scan_once(s, idx)
               ~~~~~~~~~~~~~~^^^^^^^^
json.decoder.JSONDecodeError: Illegal trailing comma before end of object: line 1 column 31 (char 30)

"Vírgula no fim não permitida antes do fim do objeto", na coluna 31: a vírgula depois do 12. No Python, {"estoque": 12,} é aceito; no JSON, não. Esse erro aparece muito em arquivos .json editados à mão. Como consertar: apague a vírgula antes do } (ou do ]).

Arquivo vazio: Expecting value
import json
from pathlib import Path

Path("vazio.json").write_text("", encoding="utf-8")

with open("vazio.json", encoding="utf-8") as arquivo:
    dados = json.load(arquivo)
$ python3 vazio.py
Traceback (most recent call last):
  File "/home/ana/vazio.py", line 7, in <module>
    dados = json.load(arquivo)
  File "/usr/lib/python3.14/json/__init__.py", line 298, in load
    return loads(fp.read(),
        cls=cls, object_hook=object_hook,
        parse_float=parse_float, parse_int=parse_int,
        parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/usr/lib/python3.14/json/__init__.py", line 352, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.14/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.14/json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

"Esperava um valor", logo na linha 1, coluna 1: o arquivo não tem nada. Um arquivo vazio não é um JSON válido (uma lista vazia seria []). Repare também que o json.load só lê o arquivo e passa o texto para o loads: é por isso que o traceback passa pelos dois. Como consertar: no primeiro uso, grave um JSON de verdade ([] ou {}); ou trate o erro, como no quadro abaixo.

Um set no meio dos dados: not JSON serializable
import json

turma = {"nome": "Turma A", "alunos": {"Bia", "Duda"}}
print(json.dumps(turma))
$ python3 conjunto.py
Traceback (most recent call last):
  File "/home/ana/conjunto.py", line 4, in <module>
    print(json.dumps(turma))
          ~~~~~~~~~~^^^^^^^
  File "/usr/lib/python3.14/json/__init__.py", line 235, in dumps
    return _default_encoder.encode(obj)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "/usr/lib/python3.14/json/encoder.py", line 202, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.14/json/encoder.py", line 263, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python3.14/json/encoder.py", line 182, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
                    f'is not JSON serializable')
TypeError: Object of type set is not JSON serializable
when serializing dict item 'alunos'

"Objeto do tipo set não é serializável em JSON", ou seja, não tem como virar JSON (veja a tabela da seção 3). A última linha, when serializing dict item 'alunos' ("ao serializar o item 'alunos' do dicionário"), aponta a chave culpada 3.14; no Python 3.13, ela não aparece, e você tem de procurar sozinho. Como consertar: transforme em lista antes: "alunos": sorted(alunos) (ou list(alunos)). O mesmo erro aparece com datas (Object of type datetime…): grave a data como texto, como você vai ver em Datas e Horas.

dump sem o arquivo
import json

contatos = [{"nome": "Bia"}]
json.dump(contatos)
$ python3 dump_sem.py
Traceback (most recent call last):
  File "/home/ana/dump_sem.py", line 4, in <module>
    json.dump(contatos)
    ~~~~~~~~~^^^^^^^^^^
TypeError: dump() missing 1 required positional argument: 'fp'

"dump() está sem 1 argumento obrigatório: 'fp'." O fp (file pointer) é o arquivo. Confundir dump com dumps é muito comum. Como consertar: se você queria o texto, json.dumps(contatos); se queria gravar, json.dump(contatos, arquivo) dentro de um with open(…, "w").

Tratando o JSON com defeito

Quando o JSON vem de fora (de um arquivo que alguém editou, de um site), o programa não deve morrer por causa dele. O json.JSONDecodeError pode ser capturado com try/except (aula try e except), e ele traz a linha e a coluna do problema:

import json

texto = '{"nome": "café", "estoque": 12,}'
try:
    dados = json.loads(texto)
except json.JSONDecodeError as erro:
    print("JSON com defeito:", erro)
    print("Linha", erro.lineno, "coluna", erro.colno)
$ python3 tratar.py
JSON com defeito: Illegal trailing comma before end of object: line 1 column 31 (char 30)
Linha 1 coluna 31

Missão: o arquivo produto.json (na segunda aba) foi escrito à mão e tem dois defeitos. Rode, leia a mensagem (linha e coluna!) e conserte o arquivo, sem mexer no programa. Ele deve mostrar Mouse sem fio custa 59.9.

import json

with open("produto.json", encoding="utf-8") as arquivo:
    produto = json.load(arquivo)

print(produto["nome"], "custa", produto["preco"])
{
    'nome': "Mouse sem fio",
    "preco": 59.9,
    "ativo": True
}
{
    "nome": "Mouse sem fio",
    "preco": 59.9,
    "ativo": true
}

Referência rápida

EscritaO que faz
import jsonTraz o módulo
json.dumps(valor)Python → texto JSON
json.loads(texto)Texto JSON → Python
json.dump(valor, arquivo)Python → arquivo (aberto com "w")
json.load(arquivo)Arquivo → Python
ensure_ascii=FalseAcentos legíveis (café em vez de café)
indent=2 / indent=4Uma informação por linha, com recuo
sort_keys=TrueGrava as chaves em ordem alfabética
except json.JSONDecodeError as erro:Captura um JSON inválido (erro.lineno, erro.colno)
python3 -m json.tool arquivo.jsonNo terminal: confere e mostra o JSON arrumado
MensagemCausa provável
Expecting property name enclosed in double quotesChave com aspas simples (ou um str(dicionario) no lugar de JSON)
Illegal trailing comma before end of object (ou of array)Vírgula sobrando antes do } ou do ]
Expecting value: line 1 column 1 (char 0)Texto ou arquivo vazio; ou um valor inválido, como True, None ou 'texto'
Object of type set is not JSON serializableTipo sem tradução (set, data, objeto): converta antes
dump() missing 1 required positional argument: 'fp'Usou dump querendo dumps, ou esqueceu o arquivo
Aparece é no lugar de éFaltou ensure_ascii=False (não é erro, só fica feio)
A tupla voltou como lista, a chave 1 voltou como '1'O JSON não tem tupla nem chave numérica

Exercícios

1. Dicionário para texto

Complete para transformar o dicionário em texto JSON, com os acentos legíveis:

texto = json.(produto, ensure_ascii=)

2. A tradução

No JSON, o None do Python se escreve:

3. Lendo do arquivo

Complete para ler a agenda do arquivo aberto:

with open("contatos.json", encoding="utf-8") as arquivo:
    contatos = json.(arquivo)

4. Qual destes textos é um JSON válido?

5. json.loads(json.dumps({"pontos": (1, 2)})) devolve:

6. Qual é a diferença entre json.dump e json.dumps?

Missão final: um contato novo na agenda

Missão: o programa lê a agenda de contatos.json e acrescenta a Duda, mas esquece de gravar: a última linha relê o arquivo e só encontra 2 contatos. Grave a lista atualizada com json.dump (com ensure_ascii=False e indent=2) antes de reler.

import json

with open("contatos.json", encoding="utf-8") as arquivo:
    contatos = json.load(arquivo)

contatos.append({"nome": "Duda", "telefone": "86 99999-0003"})

# grave aqui

with open("contatos.json", encoding="utf-8") as arquivo:
    print(len(json.load(arquivo)), "contatos")
[
  {"nome": "Bia", "telefone": "86 99999-0001"},
  {"nome": "Caio", "telefone": "86 99999-0002"}
]
import json

with open("contatos.json", encoding="utf-8") as arquivo:
    contatos = json.load(arquivo)

contatos.append({"nome": "Duda", "telefone": "86 99999-0003"})

with open("contatos.json", "w", encoding="utf-8") as arquivo:
    json.dump(contatos, arquivo, ensure_ascii=False, indent=2)

with open("contatos.json", encoding="utf-8") as arquivo:
    print(len(json.load(arquivo)), "contatos")

A agenda completa, com busca e remoção de contatos, é o Projeto: Agenda de Contatos.


Resumo

  • JSON é um formato de texto para guardar e trocar dados, entendido por qualquer linguagem. Parece um dicionário, mas só aceita aspas duplas, true/false/null e nada de vírgula no fim.
  • json.dumps (Python → texto) e json.loads (texto → Python): o s é de string.
  • json.dump(valor, arquivo) e json.load(arquivo) trabalham direto com o arquivo aberto, sempre com encoding="utf-8".
  • ensure_ascii=False deixa os acentos legíveis; indent=2 deixa o arquivo arrumado.
  • Tupla volta como lista, chave numérica volta como texto; set e datas não viram JSON sem conversão.
  • str(dicionario) não é JSON. Um JSON inválido dá json.JSONDecodeError, com a linha e a coluna do defeito.