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.
- Guardar cada contato num dicionário e todos eles numa lista; percorrer com for e enumerate().
- Montar um menu com while, input(), if / elif / else e break.
- Alinhar as colunas com f-strings e limpar o que foi digitado com strip() e lower().
- Organizar o programa em funções com return.
- Salvar e carregar com JSON, open e with e pathlib.
- Tratar os erros com try e except: número inválido, arquivo que não existe, arquivo estragado.
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:
- Os contatos na memória (uma lista de dicionários).
- O menu (
while,input,break). - Salvar e carregar (
json.dump,json.load,ensure_ascii=False). - 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
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. Oinput()devolve texto, e assim não é preciso converter comint(): se a Ana digitar uma letra, cai noelse, 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
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ãoJoã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
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
opendáFileNotFoundError("arquivo não encontrado"), e oexceptdevolve a lista vazia (aula try e except). - except json.JSONDecodeError as erro:
- O arquivo estragado da versão 3. O
as erroguarda o erro numa variável, e oprintmostra 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
salvargravaria 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
returnsai da função sem adicionar nada (aula return). - termo in c["nome"].lower()
- A busca: o
inprocura 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, oint()dáValueError, e oexcepttransforma 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
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.
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
| Tarefa | Opções | Qual usar |
|---|---|---|
| Começar sem arquivo | if 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 contatos | Lista 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 menu | Comparar com "1" · converter com int() | Comparar com o texto: nada de ValueError no menu |
| Quando salvar | A 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 remover | Pelo número da lista · pelo nome | Pelo 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: usecontato.get("email", "")(aula Dicionários) para não darKeyError. - 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
| Escrita | O 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 FileNotFoundError | O arquivo ainda não existe: começa vazio |
except json.JSONDecodeError | O arquivo está estragado: avisa e sai com sys.exit(1) |
except ValueError | Digitaram 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:
2. Gravar com acentos
Complete a opção que mantém João legível no arquivo:
3. Ler o arquivo
Complete a função do módulo json que lê o arquivo aberto:
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 Truecominput(),if/elif/elsee umbreakpara sair. Sem obreak, o programa só termina comEOFError. - O que está na memória some quando o programa fecha. Para lembrar,
json.dumpgrava a lista nocontatos.json, ejson.loadlê de volta ao começar. ensure_ascii=False(comencoding="utf-8") guardaJoão Conceiçãoem vez deJoã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 dopop, porquepop(-1)apaga o último sem erro.