Módulo 16 - Projeto final: agenda de contatos

Exportar para CSV e o menu

13 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Exportar os contatos para um arquivo CSV com o módulo csv.
  • Montar o menu principal com um laço while.
  • Tratar erros e entradas inválidas dentro do menu.
  • Reunir o programa completo em um único arquivo comentado.

Exportar para CSV

O JSON serve para o programa guardar e recarregar os dados. O CSV serve para levar os dados para fora, para uma planilha. São propósitos diferentes, e por isso a exportação é um método separado. O módulo csv da biblioteca padrão cuida dos detalhes chatos, como colocar aspas quando um campo tem vírgula. Você cria um escritor, grava uma linha de cabeçalho com os nomes das colunas e depois uma linha por contato. O parâmetro newline vazio evita linhas em branco extras no arquivo, uma recomendação da própria documentação.

import csv


class Agenda:
    # ... demais metodos como antes ...

    def exportar_csv(self, caminho):
        with open(caminho, "w", newline="", encoding="utf-8") as arquivo:
            escritor = csv.writer(arquivo)
            escritor.writerow(["nome", "telefone", "email"])
            for contato in self.contatos:
                escritor.writerow([contato.nome, contato.telefone, contato.email])

exportar_csv grava um cabeçalho e uma linha por contato, pronto para planilha.

Depois de exportar, abra o arquivo contatos.csv em uma planilha e veja os contatos organizados em colunas. Repare que aqui não há para_dicionario: o CSV é uma tabela simples, então basta uma lista com os três valores por linha, na mesma ordem do cabeçalho. Diferente do JSON, o CSV não é feito para o programa ler de volta e reconstruir objetos; ele é um ponto final, uma saída para o mundo das planilhas. Por isso não escrevemos um importar_csv: a fonte de verdade do programa continua sendo o contatos.json.

O menu que costura tudo

O menu é onde as peças se encontram. Ele carrega a agenda do arquivo ao iniciar, mostra as opções em um laço while e, a cada volta, lê o número digitado e executa a ação correspondente. Depois de adicionar ou remover, ele salva o arquivo, para que nada se perca. É também no menu que os try e except vivem, porque é aqui que dá para responder ao usuário quando um contato não existe ou quando o telefone é inválido. O while só termina quando a pessoa escolhe a opção de sair.

def menu():
    try:
        agenda = Agenda.carregar_json("contatos.json")
    except FileNotFoundError:
        agenda = Agenda()

    while True:
        print("\n1) Adicionar  2) Listar  3) Buscar  4) Remover  5) Exportar CSV  0) Sair")
        opcao = input("Opcao: ").strip()

        if opcao == "1":
            nome = input("Nome: ").strip()
            telefone = input("Telefone (so numeros): ").strip()
            if not telefone_valido(telefone):
                print("Telefone invalido. Use 10 ou 11 digitos.")
                continue
            email = input("E-mail: ").strip()
            agenda.adicionar_contato(Contato(nome, telefone, email))
            agenda.salvar_json("contatos.json")
            print("Contato adicionado.")
        elif opcao == "2":
            for contato in agenda.listar():
                print(contato)
        elif opcao == "3":
            nome = input("Nome a buscar: ").strip()
            try:
                print(agenda.buscar_por_nome(nome))
            except ContatoNaoEncontradoError:
                print(f"Nao encontrei '{nome}'.")
        elif opcao == "4":
            nome = input("Nome a remover: ").strip()
            try:
                agenda.remover(nome)
                agenda.salvar_json("contatos.json")
                print("Contato removido.")
            except ContatoNaoEncontradoError:
                print(f"Nao encontrei '{nome}'.")
        elif opcao == "5":
            agenda.exportar_csv("contatos.csv")
            print("Exportado para contatos.csv.")
        elif opcao == "0":
            print("Ate mais!")
            break
        else:
            print("Opcao invalida.")

O menu carrega, repete no while e trata cada opção, salvando quando muda algo.

Percorra a lógica com calma. O continue na validação do telefone volta ao topo do laço sem adicionar o contato, ou seja, cancela a operação e mostra o menu de novo. Os try envolvem só as chamadas que podem falhar, buscar_por_nome e remover, o que é uma boa prática: capture o mínimo necessário, para não esconder erros de outras partes. O else final pega qualquer texto que não seja uma opção válida. E o strip em cada input remove espaços acidentais, evitando que um espaço a mais atrapalhe a comparação da opção.

O programa completo

Aqui está o arquivo agenda.py inteiro, com tudo que você construiu ao longo das aulas em um só lugar, comentado. É a hora de comparar com o seu código e conferir se nada ficou para trás. Salve, rode e use a agenda de verdade: adicione contatos, feche o programa, abra de novo e veja que eles continuam lá. Você acabou de escrever um programa completo, com classes, persistência, exportação e tratamento de erros.

# agenda.py - Agenda de contatos de linha de comando
import json
import csv
import re

# Padrao de telefone: 10 ou 11 digitos, do inicio ao fim.
TELEFONE_RE = re.compile(r"^\d{10,11}$")


def telefone_valido(telefone):
    """Devolve True se o telefone tiver 10 ou 11 digitos e nada mais."""
    return TELEFONE_RE.match(telefone) is not None


class ContatoNaoEncontradoError(Exception):
    """Levantada quando um contato nao existe na agenda."""


class Contato:
    def __init__(self, nome, telefone, email=""):
        self.nome = nome
        self.telefone = telefone
        self.email = email

    def __str__(self):
        return f"{self.nome} | {self.telefone} | {self.email}"

    def para_dicionario(self):
        # Traduz o objeto para um dicionario salvavel em JSON.
        return {"nome": self.nome, "telefone": self.telefone, "email": self.email}

    @classmethod
    def de_dicionario(cls, dados):
        # Reconstroi um Contato a partir de um dicionario lido do JSON.
        return cls(dados["nome"], dados["telefone"], dados.get("email", ""))


class Agenda:
    def __init__(self):
        self.contatos = []

    def adicionar_contato(self, contato):
        self.contatos.append(contato)

    def listar(self):
        return list(self.contatos)

    def buscar_por_nome(self, nome):
        for contato in self.contatos:
            if nome.lower() in contato.nome.lower():
                return contato
        raise ContatoNaoEncontradoError(nome)

    def remover(self, nome):
        contato = self.buscar_por_nome(nome)
        self.contatos.remove(contato)

    def salvar_json(self, caminho):
        dados = [contato.para_dicionario() for contato in self.contatos]
        with open(caminho, "w", encoding="utf-8") as arquivo:
            json.dump(dados, arquivo, ensure_ascii=False, indent=2)

    @classmethod
    def carregar_json(cls, caminho):
        agenda = cls()
        with open(caminho, encoding="utf-8") as arquivo:
            dados = json.load(arquivo)
        for item in dados:
            agenda.adicionar_contato(Contato.de_dicionario(item))
        return agenda

    def exportar_csv(self, caminho):
        with open(caminho, "w", newline="", encoding="utf-8") as arquivo:
            escritor = csv.writer(arquivo)
            escritor.writerow(["nome", "telefone", "email"])
            for contato in self.contatos:
                escritor.writerow([contato.nome, contato.telefone, contato.email])


def menu():
    try:
        agenda = Agenda.carregar_json("contatos.json")
    except FileNotFoundError:
        agenda = Agenda()

    while True:
        print("\n1) Adicionar  2) Listar  3) Buscar  4) Remover  5) Exportar CSV  0) Sair")
        opcao = input("Opcao: ").strip()

        if opcao == "1":
            nome = input("Nome: ").strip()
            telefone = input("Telefone (so numeros): ").strip()
            if not telefone_valido(telefone):
                print("Telefone invalido. Use 10 ou 11 digitos.")
                continue
            email = input("E-mail: ").strip()
            agenda.adicionar_contato(Contato(nome, telefone, email))
            agenda.salvar_json("contatos.json")
            print("Contato adicionado.")
        elif opcao == "2":
            for contato in agenda.listar():
                print(contato)
        elif opcao == "3":
            nome = input("Nome a buscar: ").strip()
            try:
                print(agenda.buscar_por_nome(nome))
            except ContatoNaoEncontradoError:
                print(f"Nao encontrei '{nome}'.")
        elif opcao == "4":
            nome = input("Nome a remover: ").strip()
            try:
                agenda.remover(nome)
                agenda.salvar_json("contatos.json")
                print("Contato removido.")
            except ContatoNaoEncontradoError:
                print(f"Nao encontrei '{nome}'.")
        elif opcao == "5":
            agenda.exportar_csv("contatos.csv")
            print("Exportado para contatos.csv.")
        elif opcao == "0":
            print("Ate mais!")
            break
        else:
            print("Opcao invalida.")


if __name__ == "__main__":
    menu()

O arquivo agenda.py completo: classes, JSON, CSV, exceção e menu, tudo junto.

Teste rápido

Por que o programa salva o JSON logo após adicionar ou remover um contato?

Perguntas frequentes

Por que não existe um método importar_csv, se há exportar_csv?
Porque o CSV é uma saída para planilhas, não a fonte de verdade do programa. Quem guarda e recarrega os dados é o JSON, feito para reconstruir os objetos. Importar de CSV seria possível, mas ambíguo, já que o CSV não distingue tipos. Deixamos o JSON como formato interno único.
O que faz o newline vazio no open ao gravar CSV?
Evita linhas em branco extras entre os registros em alguns sistemas, principalmente no Windows. A documentação do módulo csv recomenda abrir o arquivo com newline vazio justamente por isso. É um detalhe pequeno que evita um arquivo com espaçamento estranho.
Para que serve o if __name__ == "__main__" no fim?
Ele faz o menu rodar apenas quando você executa o arquivo diretamente. Se outro arquivo importar agenda.py, por exemplo os testes, o menu não dispara sozinho. É o que permite reaproveitar as classes sem abrir o menu, essencial para a aula de testes.
Por que o telefone inválido usa continue em vez de encerrar?
Porque continue apenas cancela a inclusão daquele contato e volta ao menu, deixando o programa vivo para o usuário tentar de novo. Encerrar seria drástico demais para um simples erro de digitação. O programa avisa o problema e segue funcionando.
Posso adicionar uma opção de editar contato ao menu?
Pode, e é um ótimo próximo passo. Você buscaria o contato por nome, pediria os novos dados e atualizaria os atributos, salvando em seguida. O menu já tem a estrutura de elif pronta para receber a nova opção. É um dos exercícios sugeridos na aula final.
O menu trata todos os erros possíveis?
Ele trata os principais do fluxo normal: contato não encontrado e telefone inválido. Erros mais raros, como um JSON corrompido, ficam como melhoria. A ideia do projeto é mostrar o padrão de tratar no lugar certo, que você estende conforme a necessidade real.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.