Projeto: Agenda de Contatos (JSON)

A Ana quer parar de anotar telefones no caderno. Neste projeto você vai construir com ela uma agenda de contatos no terminal: listar, adicionar, buscar e remover contatos. E, o mais importante, lembrar de tudo: quando o programa fecha e abre de novo, os contatos continuam lá, guardados num arquivo contatos.json. É o primeiro programa do guia que guarda dados de verdade entre uma execução e outra.

Você vai praticar

O que vamos construir

A agenda pronta, no terminal da Ana. O que vem depois de cada Opção:, Nome:, Telefone: e Buscar: foi digitado por ela:

$ python3 agenda.py
=== Agenda ===
2 contato(s) carregado(s) de contatos.json        « os contatos de ontem

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 1
1. Ana Souza       86 99999-0000
2. João Conceição  86 98888-7777

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 2
Nome: Bia Lima
Telefone: 86 97777-1234
Bia Lima entrou na agenda.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 3
Buscar: bia                                       « minúsculas: acha a Bia mesmo assim
Bia Lima        86 97777-1234

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 0
Até logo!

Quatro versões, uma novidade em cada:

  1. Os contatos na memória (uma lista de dicionários).
  2. O menu (while, input, break).
  3. Salvar e carregar (json.dump, json.load, ensure_ascii=False).
  4. Buscar, remover e tratar os erros (funções, try/except).

Antes de começar

Crie uma pasta para o projeto e entre nela. O programa vai se chamar agenda.py, e o arquivo dos contatos, contatos.json, vai aparecer na mesma pasta:

$ mkdir -p projetos/agenda
$ cd projetos/agenda

Versão 1 · Os contatos na memória

Antes de qualquer menu, a pergunta mais importante: como guardar um contato? Um contato tem várias informações (nome, telefone), cada uma com um nome. É exatamente o que um dicionário faz: {"nome": "Ana Souza", "telefone": "86 99999-0000"}. E vários contatos, em ordem, cabem numa lista. Uma lista de dicionários é como um fichário: a lista é a caixa, cada dicionário é uma ficha.

contatos = [
    {"nome": "Ana Souza", "telefone": "86 99999-0000"},
    {"nome": "Bia Lima", "telefone": "86 98888-7777"},
    {"nome": "Caio Rocha", "telefone": "11 97777-6666"},
]

print("=== Agenda ===")
for contato in contatos:
    print(f"{contato['nome']:<15} {contato['telefone']}")
print(f"{len(contatos)} contato(s)")
contatos = [ … ]
A lista, com um contato por linha. A vírgula depois do último item é permitida, e é o costume: assim, para acrescentar uma linha, você não precisa mexer na de cima.
{"nome": …, "telefone": …}
Um dicionário: cada chave ("nome", "telefone") tem o seu valor. O telefone é texto, e não número: tem espaço e traço, e ninguém faz conta com ele.
for contato in contatos:
Passa por cada ficha da lista; a cada volta, contato é um dicionário.
contato['nome']
Pega o valor da chave "nome". Dentro da f-string de aspas duplas, a chave foi escrita com aspas simples (veja a aula f-strings).
:<15
Ocupa 15 caracteres, encostado à esquerda: os telefones ficam alinhados numa coluna.
len(contatos)
Quantos itens a lista tem: quantos contatos.
$ python3 agenda.py
=== Agenda ===
Ana Souza       86 99999-0000
Bia Lima        86 98888-7777
Caio Rocha      11 97777-6666
3 contato(s)

Quando dá errado na versão 1

A chave com letra maiúscula: KeyError
contatos = [
    {"nome": "Ana Souza", "telefone": "86 99999-0000"},
]

for contato in contatos:
    print(contato["Nome"])
$ python3 chave.py
Traceback (most recent call last):
  File "/home/ana/projetos/agenda/chave.py", line 6, in <module>
    print(contato["Nome"])
          ~~~~~~~^^^^^^^^
KeyError: 'Nome'

KeyError: 'Nome' = "erro de chave: 'Nome'". O dicionário não tem a chave "Nome", com N maiúsculo; ele tem "nome". Como nos textos, maiúsculas contam. Como consertar: escreva a chave exatamente como ela foi criada: contato["nome"].

Missão (versão 1): a agenda só tem a Ana e a Bia. No lugar do comentário, acrescente o Caio Rocha (telefone 11 97777-6666) com contatos.append(...) (aula Adicionando Itens). O append recebe um dicionário, igual aos de cima.

contatos = [
    {"nome": "Ana Souza", "telefone": "86 99999-0000"},
    {"nome": "Bia Lima", "telefone": "86 98888-7777"},
]

# Acrescente aqui o Caio Rocha (telefone 11 97777-6666)

print("=== Agenda ===")
for contato in contatos:
    print(f"{contato['nome']:<15} {contato['telefone']}")
print(f"{len(contatos)} contato(s)")
contatos = [
    {"nome": "Ana Souza", "telefone": "86 99999-0000"},
    {"nome": "Bia Lima", "telefone": "86 98888-7777"},
]

contatos.append({"nome": "Caio Rocha", "telefone": "11 97777-6666"})

print("=== Agenda ===")
for contato in contatos:
    print(f"{contato['nome']:<15} {contato['telefone']}")
print(f"{len(contatos)} contato(s)")

Versão 2 · O menu

Ninguém quer editar o programa para acrescentar um contato. A versão 2 começa com a agenda vazia e pergunta o que fazer, de novo e de novo, até a Ana escolher sair. É o mesmo esquema de menu da Calculadora: um while True que só termina com break.

contatos = []

print("=== Agenda ===")
while True:
    print()
    print("1 - Listar   2 - Adicionar   0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        if not contatos:
            print("A agenda está vazia.")
        for contato in contatos:
            print(f"{contato['nome']:<15} {contato['telefone']}")
    elif opcao == "2":
        nome = input("Nome: ").strip()
        telefone = input("Telefone: ").strip()
        contatos.append({"nome": nome, "telefone": telefone})
        print(f"{nome} entrou na agenda.")
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")
contatos = []
A lista começa vazia: os contatos vão chegar pelo teclado.
while True:
Repete para sempre… até um break.
print()
Sem nada dentro, mostra uma linha em branco: separa uma volta do menu da outra.
input("Opção: ").strip()
Lê o que foi digitado e tira os espaços das pontas (aula Métodos de Texto).
if opcao == "1":
A opção é comparada com o texto "1", não com o número 1. O input() devolve texto, e assim não é preciso converter com int(): se a Ana digitar uma letra, cai no else, sem erro.
if not contatos:
Uma lista vazia conta como "falso" (aula Booleanos): "se não há contatos…".
contatos.append({"nome": nome, "telefone": telefone})
Monta um dicionário novo com o que foi digitado e põe no fim da lista.
break
Sai do while: o programa termina.
else:
Qualquer outra coisa digitada: avisa e volta ao menu.
$ python3 agenda.py
=== Agenda ===

1 - Listar   2 - Adicionar   0 - Sair
Opção: 1
A agenda está vazia.

1 - Listar   2 - Adicionar   0 - Sair
Opção: 2
Nome: Ana Souza
Telefone: 86 99999-0000
Ana Souza entrou na agenda.

1 - Listar   2 - Adicionar   0 - Sair
Opção: 2
Nome: Bia Lima
Telefone: 86 98888-7777
Bia Lima entrou na agenda.

1 - Listar   2 - Adicionar   0 - Sair
Opção: 1
Ana Souza       86 99999-0000
Bia Lima        86 98888-7777

1 - Listar   2 - Adicionar   0 - Sair
Opção: 7
Opção inválida.

1 - Listar   2 - Adicionar   0 - Sair
Opção: 0
Até logo!

Funciona! Agora rode de novo e peça a lista:

$ python3 agenda.py
=== Agenda ===

1 - Listar   2 - Adicionar   0 - Sair
Opção: 1
A agenda está vazia.                              « a Ana e a Bia sumiram

1 - Listar   2 - Adicionar   0 - Sair
Opção: 0
Até logo!

A lista contatos vive na memória do programa. Quando o programa termina, a memória é liberada, e tudo o que estava nela se perde. É como escrever num quadro branco que alguém apaga toda noite. A versão 3 passa os contatos para o "caderno": um arquivo.

Quando dá errado na versão 2

A entrada acabou: EOFError

Se o input() pede uma linha e não há mais nada para ler, o Python não tem o que devolver. Dá para provocar isso mandando ao programa só uma linha, com um | do terminal, ou apertando Ctrl+D no lugar de digitar a opção:

$ printf '1\n' | python3 agenda.py
=== Agenda ===

1 - Listar   2 - Adicionar   0 - Sair
Opção: A agenda está vazia.

1 - Listar   2 - Adicionar   0 - Sair
Opção: Traceback (most recent call last):
  File "/home/ana/projetos/agenda/agenda.py", line 7, in <module>
    opcao = input("Opção: ").strip()
            ~~~~~^^^^^^^^^^^
EOFError: EOF when reading a line

O 1 foi lido na primeira volta. Na segunda, o input() encontrou o fim da entrada: EOFError: EOF when reading a line, "fim do arquivo (end of file) ao ler uma linha". (O 1 não aparece depois de Opção: porque ele não foi digitado: veio pelo |.) No uso normal, com a Ana digitando, isso só acontece com Ctrl+D. Um menu sem break termina sempre assim: é o que a missão abaixo mostra.

Missão (versão 2): a entrada desta missão adiciona a Ana, lista e escolhe 0. O programa diz "Até logo!"… e continua pedindo opção, até dar EOFError. Falta uma palavra na opção "0". Conserte.

contatos = []

print("=== Agenda ===")
while True:
    print()
    print("1 - Listar   2 - Adicionar   0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        if not contatos:
            print("A agenda está vazia.")
        for contato in contatos:
            print(f"{contato['nome']:<15} {contato['telefone']}")
    elif opcao == "2":
        nome = input("Nome: ").strip()
        telefone = input("Telefone: ").strip()
        contatos.append({"nome": nome, "telefone": telefone})
        print(f"{nome} entrou na agenda.")
    elif opcao == "0":
        print("Até logo!")
    else:
        print("Opção inválida.")
contatos = []

print("=== Agenda ===")
while True:
    print()
    print("1 - Listar   2 - Adicionar   0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        if not contatos:
            print("A agenda está vazia.")
        for contato in contatos:
            print(f"{contato['nome']:<15} {contato['telefone']}")
    elif opcao == "2":
        nome = input("Nome: ").strip()
        telefone = input("Telefone: ").strip()
        contatos.append({"nome": nome, "telefone": telefone})
        print(f"{nome} entrou na agenda.")
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")

Versão 3 · Salvar e carregar (contatos.json)

Para a agenda lembrar dos contatos, o programa precisa fazer duas coisas: ao começar, ler os contatos do arquivo; a cada contato novo, gravar a lista inteira no arquivo. O formato do arquivo é o JSON (aula JSON): um texto que se parece muito com listas e dicionários do Python, e que o módulo json converte nos dois sentidos.

import json
from pathlib import Path

ARQUIVO = Path("contatos.json")


def carregar():
    if not ARQUIVO.exists():
        return []
    with open(ARQUIVO, encoding="utf-8") as arquivo:
        return json.load(arquivo)


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


contatos = carregar()
print("=== Agenda ===")
print(f"{len(contatos)} contato(s) carregado(s) de {ARQUIVO}")

while True:
    print()
    print("1 - Listar   2 - Adicionar   0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        if not contatos:
            print("A agenda está vazia.")
        for contato in contatos:
            print(f"{contato['nome']:<15} {contato['telefone']}")
    elif opcao == "2":
        nome = input("Nome: ").strip()
        telefone = input("Telefone: ").strip()
        contatos.append({"nome": nome, "telefone": telefone})
        salvar(contatos)
        print(f"{nome} entrou na agenda (salvo em {ARQUIVO}).")
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")
import json
Traz o módulo que lê e escreve JSON (aula Módulos e import).
from pathlib import Path
Traz o Path, que representa um caminho de arquivo (aula pathlib).
ARQUIVO = Path("contatos.json")
O nome do arquivo, numa variável só. O nome em MAIÚSCULAS é o costume para um valor que não muda durante o programa (uma "constante"). Sem pasta no caminho, o arquivo fica na pasta em que você rodou o python3.
def carregar():
Uma função que devolve a lista de contatos.
if not ARQUIVO.exists(): return []
Na primeira vez, o arquivo ainda não existe: começa com a lista vazia.
with open(ARQUIVO, encoding="utf-8") as arquivo:
Abre o arquivo para ler, com a codificação UTF-8 (a que guarda acentos), e fecha sozinho no fim do bloco (aula Abrindo Arquivos).
json.load(arquivo)
Lê o texto JSON do arquivo e devolve o valor Python: aqui, uma lista de dicionários.
open(ARQUIVO, "w", …)
O "w" (de write) abre para escrever, apagando o que havia. Tudo bem: a lista inteira vai ser gravada de novo.
json.dump(contatos, arquivo, …)
Faz o contrário do load: transforma a lista em texto JSON e grava no arquivo.
ensure_ascii=False
Grava os acentos como eles são: João, e não João. Veja o porquê logo abaixo.
indent=2
Quebra as linhas e recua 2 espaços por nível: o arquivo fica legível para gente.
salvar(contatos)
Logo depois de cada append: o arquivo está sempre em dia, mesmo que o programa feche de repente.

A Ana começa sem arquivo nenhum, adiciona dois contatos e sai:

$ python3 agenda.py
=== Agenda ===
0 contato(s) carregado(s) de contatos.json

1 - Listar   2 - Adicionar   0 - Sair
Opção: 2
Nome: Ana Souza
Telefone: 86 99999-0000
Ana Souza entrou na agenda (salvo em contatos.json).

1 - Listar   2 - Adicionar   0 - Sair
Opção: 2
Nome: João Conceição
Telefone: 86 98888-7777
João Conceição entrou na agenda (salvo em contatos.json).

1 - Listar   2 - Adicionar   0 - Sair
Opção: 0
Até logo!

Agora existe um arquivo na pasta. O cat mostra o que tem dentro:

$ cat contatos.json
[
  {
    "nome": "Ana Souza",
    "telefone": "86 99999-0000"
  },
  {
    "nome": "João Conceição",
    "telefone": "86 98888-7777"
  }
]$ python3 agenda.py                                 « o $ colou no ]: veja abaixo
=== Agenda ===
2 contato(s) carregado(s) de contatos.json        « lembrou!

1 - Listar   2 - Adicionar   0 - Sair
Opção: 1
Ana Souza       86 99999-0000
João Conceição  86 98888-7777

1 - Listar   2 - Adicionar   0 - Sair
Opção: 0
Até logo!

O arquivo é a lista de dicionários, escrita como texto: colchetes, chaves, aspas duplas. E, rodando de novo, o programa achou os 2 contatos. Repare num detalhe: o prompt $ apareceu grudado no ]. O json.dump não põe uma quebra de linha no fim do arquivo, e o cat mostra o arquivo exatamente como ele é. Não faz mal nenhum ao programa.

Por que ensure_ascii=False?

Sem essa opção, o json troca toda letra que não é do inglês (o ASCII) por um código \u, aquele da aula Caracteres de Escape. No modo interativo:

$ python3
Python 3.14.4 (main, Aug 20 2026, 10:41:58) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import json
>>> json.dumps({"nome": "João"})
'{"nome": "Jo\\u00e3o"}'
>>> json.dumps({"nome": "João"}, ensure_ascii=False)
'{"nome": "João"}'
>>> exit()

(O json.dumps, com s de string, devolve o JSON como texto em vez de gravar num arquivo. A barra aparece dobrada porque o modo interativo mostra o texto "como seria escrito no código".) Num arquivo gravado sem a opção, o João fica assim:

$ cat teste.json; echo
[
  {
    "nome": "João Conceição",
    "telefone": "86 98888-7777"
  }
]

Não é um erro: o json.load lê João de volta como João, e o programa funciona igual. Mas o arquivo fica ilegível para quem abre num editor de texto, e uma agenda brasileira tem acento em todo canto. Com ensure_ascii=False (e o encoding="utf-8" no open), o arquivo guarda João Conceição, como se escreve.

Quando dá errado na versão 3

Um arquivo estragado: JSONDecodeError

A Ana abriu o contatos.json num editor para corrigir um telefone e, sem querer, apagou o ] do fim:

$ cat contatos.json; echo
[{"nome": "Ana Souza", "telefone": "86 99999-0000"}
$ printf '0\n' | python3 agenda.py
Traceback (most recent call last):
  File "/home/ana/projetos/agenda/agenda.py", line 19, in <module>
    contatos = carregar()
  File "/home/ana/projetos/agenda/agenda.py", line 11, in carregar
    return 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 361, in raw_decode
    obj, end = self.scan_once(s, idx)
               ~~~~~~~~~~~~~~^^^^^^^^
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 1 column 52 (char 51)

(O printf '0\n' | só deixa pronto um 0 para sair do menu, caso o programa chegasse lá. Não chegou.) Um traceback comprido, mas só as duas primeiras partes são do programa da Ana: a linha 19 chamou carregar(), e a linha 11, dentro dela, chamou json.load. O resto (os arquivos em /usr/lib/python3.14/json/) é o caminho por dentro do próprio módulo json, e você pode pular. A última linha é o que importa:

json.decoder.JSONDecodeError:
"Erro ao decodificar o JSON": o texto do arquivo não é um JSON válido.
Expecting ',' delimiter:
"Esperava o separador ,": depois do primeiro contato, o JSON esperava uma vírgula (para vir outro contato) ou o ], e o arquivo acabou.
line 1 column 52 (char 51)
Onde: linha 1, coluna 52 (o caractere de número 51, contando do zero), logo depois do }.

Como consertar: devolva o ] ao fim do arquivo. E a versão 4 vai tratar esse erro com uma mensagem clara, em vez do traceback.

Missão (versão 3): a aba contatos.json já tem a Ana, e a entrada da missão adiciona a Maria Conceição. Mas a função salvar esqueceu uma opção, e o arquivo grava Conceição. Acrescente ensure_ascii=False no json.dump. Depois de rodar, clique na aba contatos.json para ver o arquivo.

import json
from pathlib import Path

ARQUIVO = Path("contatos.json")


def carregar():
    if not ARQUIVO.exists():
        return []
    with open(ARQUIVO, encoding="utf-8") as arquivo:
        return json.load(arquivo)


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


contatos = carregar()
print("=== Agenda ===")
print(f"{len(contatos)} contato(s) carregado(s) de {ARQUIVO}")

while True:
    print()
    print("1 - Listar   2 - Adicionar   0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        if not contatos:
            print("A agenda está vazia.")
        for contato in contatos:
            print(f"{contato['nome']:<15} {contato['telefone']}")
    elif opcao == "2":
        nome = input("Nome: ").strip()
        telefone = input("Telefone: ").strip()
        contatos.append({"nome": nome, "telefone": telefone})
        salvar(contatos)
        print(f"{nome} entrou na agenda (salvo em {ARQUIVO}).")
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")
[
  {
    "nome": "Ana Souza",
    "telefone": "86 99999-0000"
  }
]
import json
from pathlib import Path

ARQUIVO = Path("contatos.json")


def carregar():
    if not ARQUIVO.exists():
        return []
    with open(ARQUIVO, encoding="utf-8") as arquivo:
        return json.load(arquivo)


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


contatos = carregar()
print("=== Agenda ===")
print(f"{len(contatos)} contato(s) carregado(s) de {ARQUIVO}")

while True:
    print()
    print("1 - Listar   2 - Adicionar   0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        if not contatos:
            print("A agenda está vazia.")
        for contato in contatos:
            print(f"{contato['nome']:<15} {contato['telefone']}")
    elif opcao == "2":
        nome = input("Nome: ").strip()
        telefone = input("Telefone: ").strip()
        contatos.append({"nome": nome, "telefone": telefone})
        salvar(contatos)
        print(f"{nome} entrou na agenda (salvo em {ARQUIVO}).")
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")

Versão 4 · Buscar, remover e tratar os erros

A última versão acrescenta as opções Buscar e Remover, e cuida dos erros que podem acontecer com quem usa o programa: digitar uma letra onde se espera um número, escolher um número que não existe, deixar o nome em branco, o arquivo estragado da versão 3. Como o programa cresceu, cada opção do menu virou uma função: o while lá embaixo ficou curto, e cada pedaço pode ser lido sozinho.

import json
import sys
from pathlib import Path

ARQUIVO = Path("contatos.json")


def carregar():
    try:
        with open(ARQUIVO, encoding="utf-8") as arquivo:
            return json.load(arquivo)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError as erro:
        print(f"O arquivo {ARQUIVO} está com defeito: {erro}")
        print("Conserte o arquivo (ou renomeie) e rode de novo.")
        sys.exit(1)


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


def listar(contatos):
    if not contatos:
        print("A agenda está vazia.")
    for numero, contato in enumerate(contatos, start=1):
        print(f"{numero}. {contato['nome']:<15} {contato['telefone']}")


def adicionar(contatos):
    nome = input("Nome: ").strip()
    telefone = input("Telefone: ").strip()
    if not nome:
        print("O nome não pode ficar em branco.")
        return
    contatos.append({"nome": nome, "telefone": telefone})
    salvar(contatos)
    print(f"{nome} entrou na agenda.")


def buscar(contatos):
    termo = input("Buscar: ").strip().lower()
    achados = [c for c in contatos if termo in c["nome"].lower()]
    if not achados:
        print("Ninguém encontrado.")
    for contato in achados:
        print(f"{contato['nome']:<15} {contato['telefone']}")


def remover(contatos):
    listar(contatos)
    try:
        numero = int(input("Número do contato a remover: "))
    except ValueError:
        print("Digite o número que aparece na lista.")
        return
    if not 1 <= numero <= len(contatos):
        print("Não existe contato com esse número.")
        return
    contato = contatos.pop(numero - 1)
    salvar(contatos)
    print(f"{contato['nome']} saiu da agenda.")


contatos = carregar()
print("=== Agenda ===")
print(f"{len(contatos)} contato(s) carregado(s) de {ARQUIVO}")

while True:
    print()
    print("1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        listar(contatos)
    elif opcao == "2":
        adicionar(contatos)
    elif opcao == "3":
        buscar(contatos)
    elif opcao == "4":
        remover(contatos)
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")

As novidades, função por função:

carregar(): try … except FileNotFoundError
Em vez de perguntar antes se o arquivo existe, o programa tenta abrir; se não existe, o open dá FileNotFoundError ("arquivo não encontrado"), e o except devolve a lista vazia (aula try e except).
except json.JSONDecodeError as erro:
O arquivo estragado da versão 3. O as erro guarda o erro numa variável, e o print mostra a explicação dele (Expecting ',' delimiter…) numa frase só, sem traceback.
sys.exit(1)
Encerra o programa com o código de saída 1, o sinal de "deu problema" (aula os e sys). Por que não começar com a agenda vazia? Porque, no primeiro contato novo, o salvar gravaria a lista vazia por cima do arquivo, e os contatos estragados, que ainda dá para consertar, se perderiam de vez.
enumerate(contatos, start=1)
Dá o número de cada contato junto com ele, começando do 1 (aula enumerate() e zip()). É o número que a Ana digita para remover.
adicionar(): if not nome: … return
Um texto vazio conta como falso: se o nome ficou em branco, avisa e o return sai da função sem adicionar nada (aula return).
termo in c["nome"].lower()
A busca: o in procura o pedaço em qualquer lugar do nome (aula is e in), com os dois lados em minúsculas. "jo" acha João; "bia" acha Bia Lima.
[c for c in contatos if …]
Uma list comprehension: a lista dos contatos que passam no teste.
remover(): int(input(…))
Aqui sim é preciso um número, para achar a posição na lista. Se a Ana digitar abc, o int() dá ValueError, e o except transforma isso num aviso.
if not 1 <= numero <= len(contatos):
Confere se o número está entre 1 e a quantidade de contatos, com uma comparação encadeada. A seção "Quando dá errado" mostra por que isso é indispensável.
contatos.pop(numero - 1)
Tira o contato da lista e devolve quem saiu (aula Removendo Itens). O - 1 é porque a Ana conta a partir do 1, e os índices começam no 0.

A Ana testou tudo, de propósito, inclusive os erros:

$ python3 agenda.py
=== Agenda ===
2 contato(s) carregado(s) de contatos.json

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 3
Buscar: jo
João Conceição  86 98888-7777

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 3
Buscar: Zé
Ninguém encontrado.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 2
Nome:                                             « Enter sem digitar nada
Telefone:
O nome não pode ficar em branco.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 2
Nome: Bia Lima
Telefone: 86 97777-1234
Bia Lima entrou na agenda.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 1
1. Ana Souza       86 99999-0000
2. João Conceição  86 98888-7777
3. Bia Lima        86 97777-1234

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 4
1. Ana Souza       86 99999-0000
2. João Conceição  86 98888-7777
3. Bia Lima        86 97777-1234
Número do contato a remover: abc
Digite o número que aparece na lista.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 4
1. Ana Souza       86 99999-0000
2. João Conceição  86 98888-7777
3. Bia Lima        86 97777-1234
Número do contato a remover: 9
Não existe contato com esse número.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 4
1. Ana Souza       86 99999-0000
2. João Conceição  86 98888-7777
3. Bia Lima        86 97777-1234
Número do contato a remover: 1
Ana Souza saiu da agenda.

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 1
1. João Conceição  86 98888-7777
2. Bia Lima        86 97777-1234

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 0
Até logo!
$ cat contatos.json; echo
[
  {
    "nome": "João Conceição",
    "telefone": "86 98888-7777"
  },
  {
    "nome": "Bia Lima",
    "telefone": "86 97777-1234"
  }
]

E com o arquivo estragado da versão 3, agora o programa explica o problema e para, sem traceback e sem apagar nada:

$ python3 agenda.py
O arquivo contatos.json está com defeito: Expecting ',' delimiter: line 1 column 52 (char 51)
Conserte o arquivo (ou renomeie) e rode de novo.
$ echo $?
1

O echo $? mostra o código de saída do último programa: o 1 do sys.exit(1).

Quando dá errado na versão 4

Sem o try: ValueError

Antes de pôr o try, a função remover tinha só numero = int(input("Número do contato a remover: ")). Bastava uma letra:

$ python3 agenda.py
=== Agenda ===
2 contato(s) carregado(s) de contatos.json

1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair
Opção: 4
1. João Conceição  86 98888-7777
2. Bia Lima        86 97777-1234
Número do contato a remover: abc
Traceback (most recent call last):
  File "/home/ana/projetos/agenda/agenda.py", line 79, in <module>
    remover(contatos)
    ~~~~~~~^^^^^^^^^^
  File "/home/ana/projetos/agenda/agenda.py", line 54, in remover
    numero = int(input("Número do contato a remover: "))
             ~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'

O traceback conta o caminho de cima para baixo: a linha 79 (o while) chamou remover, e a linha 54, dentro de remover, falhou. invalid literal for int() with base 10: 'abc' = "valor inválido para o int() na base 10: 'abc'": abc não é um número. E o pior: o programa inteiro fechou por causa de uma tecla errada. Como consertar: o try/except ValueError da versão 4.

Sem a conferência do número: o 0 apaga o último

Por que conferir 1 <= numero <= len(contatos), se o pop de uma posição que não existe já dá erro (IndexError)? Por causa do zero. Veja o que pop(0 - 1) faz:

$ python3
Python 3.14.4 (main, Aug 20 2026, 10:41:58) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> contatos = ["Ana", "João", "Bia"]
>>> numero = 0
>>> contatos.pop(numero - 1)
'Bia'
>>> contatos
['Ana', 'João']
>>> exit()

Nenhum erro, e a Bia foi apagada! 0 - 1 é -1, e o índice -1 é o último da lista (os índices negativos contam do fim, como em Índices e Fatias). Um except IndexError nunca pegaria esse caso, porque não há erro. Como consertar: confira a faixa antes do pop, como a versão 4 faz.

Missão (versão 4): a função remover está sem o try, e a entrada desta missão digita abc no número: o programa quebra com ValueError. Proteja o int(input(...)) com try/except ValueError, mostrando Digite o número que aparece na lista. e voltando ao menu com return. Em seguida, a entrada remove o contato 1 (a Ana).

import json
import sys
from pathlib import Path

ARQUIVO = Path("contatos.json")


def carregar():
    try:
        with open(ARQUIVO, encoding="utf-8") as arquivo:
            return json.load(arquivo)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError as erro:
        print(f"O arquivo {ARQUIVO} está com defeito: {erro}")
        print("Conserte o arquivo (ou renomeie) e rode de novo.")
        sys.exit(1)


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


def listar(contatos):
    if not contatos:
        print("A agenda está vazia.")
    for numero, contato in enumerate(contatos, start=1):
        print(f"{numero}. {contato['nome']:<15} {contato['telefone']}")


def adicionar(contatos):
    nome = input("Nome: ").strip()
    telefone = input("Telefone: ").strip()
    if not nome:
        print("O nome não pode ficar em branco.")
        return
    contatos.append({"nome": nome, "telefone": telefone})
    salvar(contatos)
    print(f"{nome} entrou na agenda.")


def buscar(contatos):
    termo = input("Buscar: ").strip().lower()
    achados = [c for c in contatos if termo in c["nome"].lower()]
    if not achados:
        print("Ninguém encontrado.")
    for contato in achados:
        print(f"{contato['nome']:<15} {contato['telefone']}")


def remover(contatos):
    listar(contatos)
    numero = int(input("Número do contato a remover: "))
    if not 1 <= numero <= len(contatos):
        print("Não existe contato com esse número.")
        return
    contato = contatos.pop(numero - 1)
    salvar(contatos)
    print(f"{contato['nome']} saiu da agenda.")


contatos = carregar()
print("=== Agenda ===")
print(f"{len(contatos)} contato(s) carregado(s) de {ARQUIVO}")

while True:
    print()
    print("1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        listar(contatos)
    elif opcao == "2":
        adicionar(contatos)
    elif opcao == "3":
        buscar(contatos)
    elif opcao == "4":
        remover(contatos)
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")
[
  {
    "nome": "Ana Souza",
    "telefone": "86 99999-0000"
  },
  {
    "nome": "João Conceição",
    "telefone": "86 98888-7777"
  }
]
import json
import sys
from pathlib import Path

ARQUIVO = Path("contatos.json")


def carregar():
    try:
        with open(ARQUIVO, encoding="utf-8") as arquivo:
            return json.load(arquivo)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError as erro:
        print(f"O arquivo {ARQUIVO} está com defeito: {erro}")
        print("Conserte o arquivo (ou renomeie) e rode de novo.")
        sys.exit(1)


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


def listar(contatos):
    if not contatos:
        print("A agenda está vazia.")
    for numero, contato in enumerate(contatos, start=1):
        print(f"{numero}. {contato['nome']:<15} {contato['telefone']}")


def adicionar(contatos):
    nome = input("Nome: ").strip()
    telefone = input("Telefone: ").strip()
    if not nome:
        print("O nome não pode ficar em branco.")
        return
    contatos.append({"nome": nome, "telefone": telefone})
    salvar(contatos)
    print(f"{nome} entrou na agenda.")


def buscar(contatos):
    termo = input("Buscar: ").strip().lower()
    achados = [c for c in contatos if termo in c["nome"].lower()]
    if not achados:
        print("Ninguém encontrado.")
    for contato in achados:
        print(f"{contato['nome']:<15} {contato['telefone']}")


def remover(contatos):
    listar(contatos)
    try:
        numero = int(input("Número do contato a remover: "))
    except ValueError:
        print("Digite o número que aparece na lista.")
        return
    if not 1 <= numero <= len(contatos):
        print("Não existe contato com esse número.")
        return
    contato = contatos.pop(numero - 1)
    salvar(contatos)
    print(f"{contato['nome']} saiu da agenda.")


contatos = carregar()
print("=== Agenda ===")
print(f"{len(contatos)} contato(s) carregado(s) de {ARQUIVO}")

while True:
    print()
    print("1 - Listar  2 - Adicionar  3 - Buscar  4 - Remover  0 - Sair")
    opcao = input("Opção: ").strip()

    if opcao == "1":
        listar(contatos)
    elif opcao == "2":
        adicionar(contatos)
    elif opcao == "3":
        buscar(contatos)
    elif opcao == "4":
        remover(contatos)
    elif opcao == "0":
        print("Até logo!")
        break
    else:
        print("Opção inválida.")

Depois de cumprir a missão, troque a entrada (o campo Entrada da caixa) por 4, 0, 0 e veja a conferência da faixa em ação.


Formas diferentes de fazer a mesma coisa

TarefaOpçõesQual usar
Começar sem arquivoif not ARQUIVO.exists(): return [] (versão 3) · try … except FileNotFoundError (versão 4)As duas funcionam. O try é o mais comum em Python ("é mais fácil pedir desculpas do que permissão"), e evita a pequena chance de o arquivo sumir entre a pergunta e o open
Guardar os contatosLista de dicionários · um dicionário {nome: telefone}A lista: cada contato pode ganhar mais campos (email, aniversário), e dois contatos podem ter o mesmo nome
Ler o menuComparar com "1" · converter com int()Comparar com o texto: nada de ValueError no menu
Quando salvarA cada mudança · só ao sair (opção 0)A cada mudança: se o programa fechar de repente (Ctrl+C, falta de luz), nada se perde
Escolher quem removerPelo número da lista · pelo nomePelo número: nomes podem se repetir, e ninguém gosta de digitar "João Conceição" inteiro

Ideias para continuar

  • Um campo "email" em cada contato. Os contatos antigos do arquivo não têm essa chave: use contato.get("email", "") (aula Dicionários) para não dar KeyError.
  • Uma opção Editar, que troca o telefone de um contato escolhido pelo número.
  • Listar em ordem alfabética, com sorted(contatos, key=lambda c: c["nome"]) (aulas Ordenando Listas e lambda).
  • Avisar quando o nome já existe, antes de adicionar.
  • Confirmar a remoção: Remover Ana Souza? (s/n).

Referência rápida do projeto

EscritaO que faz no projeto
{"nome": nome, "telefone": telefone}Um contato (dicionário)
contatos.append(contato)Põe o contato no fim da lista
contatos.pop(numero - 1)Tira o contato da posição e devolve quem saiu
json.load(arquivo)Lê o JSON do arquivo e devolve a lista
json.dump(contatos, arquivo, ensure_ascii=False, indent=2)Grava a lista no arquivo, com acentos de verdade e recuo de 2 espaços
open(ARQUIVO, "w", encoding="utf-8")Abre para escrever (apaga o que havia), em UTF-8
except FileNotFoundErrorO arquivo ainda não existe: começa vazio
except json.JSONDecodeErrorO arquivo está estragado: avisa e sai com sys.exit(1)
except ValueErrorDigitaram algo que não é número
enumerate(contatos, start=1)Numera os contatos a partir do 1

Exercícios

1. O telefone do contato

Complete para mostrar o telefone de um contato:

print(contato[])

2. Gravar com acentos

Complete a opção que mantém João legível no arquivo:

json.dump(contatos, arquivo, , indent=2)

3. Ler o arquivo

Complete a função do módulo json que lê o arquivo aberto:

contatos = json.(arquivo)

4. Na versão 2, por que os contatos somem quando o programa fecha?

5. Sem a conferência da faixa, o que contatos.pop(0 - 1) faz?

6. O contatos.json está estragado. Por que a versão 4 sai com sys.exit(1) em vez de começar com a agenda vazia?


Resumo

  • Cada contato é um dicionário; a agenda é uma lista de dicionários.
  • O menu é um while True com input(), if/elif/else e um break para sair. Sem o break, o programa só termina com EOFError.
  • O que está na memória some quando o programa fecha. Para lembrar, json.dump grava a lista no contatos.json, e json.load lê de volta ao começar.
  • ensure_ascii=False (com encoding="utf-8") guarda João Conceição em vez de João Conceição.
  • Os erros de quem usa o programa viram mensagens: FileNotFoundError (começa vazio), JSONDecodeError (avisa e sai sem apagar nada), ValueError (número inválido). E a faixa do número é conferida antes do pop, porque pop(-1) apaga o último sem erro.