00
introdução

Antes de começar

O que você vai aprender, como está organizado e como pensar como programador.

O que é esta trilha

Esta trilha ensina Programação Orientada a Objetos (OOP) em Python de forma progressiva e prática. Cada parte constrói sobre a anterior. O objetivo final é criar um pequeno sistema de cadastro de eventos e aniversariantes rodando no terminal.

Você não vai decorar conceitos abstratos. Vai construir coisas e entender por que cada decisão existe.

O que você precisa saber antes

Como pensar em OOP — a versão técnica

Em vez de organizar código como sequências de passos, OOP organiza código em torno de entidades — objetos que têm dados (atributos) e comportamentos (métodos). Um Carro sabe sua velocidade e sabe acelerar. Uma Pessoa tem um nome e sabe se apresentar.

Analogia do dia a dia

Imagine uma ficha de cadastro em papel. Ela tem campos (nome, telefone, endereço) e define o que pode ser escrito ali. Uma classe é essa ficha em branco. Quando você preenche uma ficha específica com os dados de "João Silva", você criou um objeto — uma instância dessa classe.

Como usar esta trilha

Cada exercício tem uma seção de conceitos, uma explicação, o código completo e a resposta do desafio oculta. Tente sempre fazer o desafio antes de revelar a resposta.

Boas práticas desde o início

Use type hints em tudo. Use f-strings. Use pathlib se precisar de paths. Prefira clareza a esperteza. Código que outros entendem vale mais que código "inteligente".

01
fundamentos

Classe, objeto e atributos

Criar a primeira classe, instanciar objetos, entender self e __init__.

Estimativa: 1–2 horas

Conceitos desta parte

Por que self existe?

Em Python, quando você chama pessoa.apresentar(), por baixo dos panos isso vira Pessoa.apresentar(pessoa). O self é o Python passando o objeto como primeiro argumento automaticamente. Ele não é mágico — é só uma convenção de nome.

Pense assim

Você tem uma receita de bolo (classe). Quando faz um bolo de chocolate (objeto), a receita precisa saber em qual tigela está trabalhando — essa "tigela atual" é o self.

1.1 A primeira classe — Cachorro
class __init__ self type hints

Objetivo

Criar a classe mais simples possível. Entender o que __init__ faz e por que existe.

Explicação

__init__ é chamado automaticamente quando você faz Cachorro("Rex"). Python cria o objeto vazio e chama __init__ com esse objeto como self — para que você possa guardar os dados nele.

Python — cachorro.py
class Cachorro:
    def __init__(self, nome: str, raca: str) -> None:
        self.nome = nome      # atributo guardado no objeto
        self.raca = raca

    def latir(self) -> str:
        return f"{self.nome} diz: Au au!"


# criando objetos
rex = Cachorro("Rex", "Labrador")
luna = Cachorro("Luna", "Poodle")

print(rex.latir())    # Rex diz: Au au!
print(luna.raca)     # Poodle

Dica

-> None em __init__ é correto: ele nunca retorna nada. Type hints não mudam o comportamento do código — são uma documentação viva que editores e ferramentas conseguem ler.

Erro comum

Esquecer o self como primeiro parâmetro. Se você fizer def __init__(nome, raca), quando chamar Cachorro("Rex", "Lab"), Python vai passar o objeto em nome e "Rex" em raca — confusão total.

Desafio

Adicione um atributo idade: int e um método info() que retorna uma string no formato:
"Rex (Labrador, 3 anos)"

Resposta
class Cachorro:
    def __init__(self, nome: str, raca: str, idade: int) -> None:
        self.nome = nome
        self.raca = raca
        self.idade = idade

    def info(self) -> str:
        return f"{self.nome} ({self.raca}, {self.idade} anos)"


rex = Cachorro("Rex", "Labrador", 3)
print(rex.info())  # Rex (Labrador, 3 anos)
1.2 Método __str__ — o objeto se apresenta
__str__ print() representação

Por que __str__ existe

Quando você faz print(rex) sem definir __str__, Python mostra algo como <__main__.Cachorro object at 0x7f...> — pouco útil. Definindo __str__, você controla como o objeto se apresenta em texto.

Analogia

É como um crachá. Sem ele, alguém te veria como "humano número 38472". Com ele, você se apresenta como "Djalma, desenvolvedor". O objeto sabe como se descrever.

Python
class Produto:
    def __init__(self, nome: str, preco: float) -> None:
        self.nome = nome
        self.preco = preco

    def __str__(self) -> str:
        return f"{self.nome} — R$ {self.preco:.2f}"


caneta = Produto("Caneta Azul", 3.50)
print(caneta)  # Caneta Azul — R$ 3.50

Nota

Existe também __repr__, usado para depuração. A convenção: __str__ é para humanos, __repr__ é para programadores. Por ora, __str__ é suficiente.

Desafio

Crie uma classe Livro com titulo, autor e ano. Implemente __str__ para retornar:
"Dom Casmurro — Machado de Assis (1899)"

Resposta
class Livro:
    def __init__(self, titulo: str, autor: str, ano: int) -> None:
        self.titulo = titulo
        self.autor = autor
        self.ano = ano

    def __str__(self) -> str:
        return f"{self.titulo} — {self.autor} ({self.ano})"


dom = Livro("Dom Casmurro", "Machado de Assis", 1899)
print(dom)  # Dom Casmurro — Machado de Assis (1899)
02
estado e comportamento

Objetos que mudam e interagem

Métodos que alteram estado. Listas de objetos. Encapsulamento básico.

Estimativa: 2–3 horas

Conceitos desta parte

O que é encapsulamento — a versão técnica

Esconder detalhes internos e expor apenas uma interface controlada. Um objeto decide o que mostra e como deixa que mudem seus dados. Isso protege o estado interno contra mudanças acidentais.

Analogia

Um velocímetro de carro. Você vê a velocidade — mas não pode alterar o valor diretamente "de fora". Quem muda a velocidade é o motor, com suas regras. Você só lê. O carro encapsula o estado interno.

2.1 Conta bancária — estado mutável
estado métodos validação

Objetivo

Criar um objeto cujo estado muda ao longo do tempo. Entender que métodos são responsáveis por garantir que mudanças sejam válidas.

Python — conta.py
class ContaBancaria:
    def __init__(self, titular: str, saldo_inicial: float = 0.0) -> None:
        self.titular = titular
        self._saldo = saldo_inicial  # _ = convencao "nao mexa diretamente"

    def depositar(self, valor: float) -> None:
        if valor <= 0:
            raise ValueError("Valor de depósito deve ser positivo")
        self._saldo += valor

    def sacar(self, valor: float) -> None:
        if valor > self._saldo:
            raise ValueError("Saldo insuficiente")
        self._saldo -= valor

    @property
    def saldo(self) -> float:
        return self._saldo

    def __str__(self) -> str:
        return f"Conta de {self.titular} — R$ {self._saldo:.2f}"


conta = ContaBancaria("Maria", 100.0)
conta.depositar(50.0)
print(conta.saldo)  # 150.0  — lê via @property
print(conta)        # Conta de Maria — R$ 150.00

Por que usar @property?

@property permite que conta.saldo pareça um atributo normal, mas por dentro executa um método. Você pode adicionar lógica depois sem mudar como quem usa a classe acessa o valor.

Desafio

Adicione um histórico de transações: uma lista que registra cada operação como string. Ex: "Depósito: R$ 50.00", "Saque: R$ 20.00". Crie um método extrato() que imprime o histórico.

Resposta
class ContaBancaria:
    def __init__(self, titular: str, saldo_inicial: float = 0.0) -> None:
        self.titular = titular
        self._saldo = saldo_inicial
        self._historico: list[str] = []

    def depositar(self, valor: float) -> None:
        if valor <= 0:
            raise ValueError("Valor deve ser positivo")
        self._saldo += valor
        self._historico.append(f"Depósito: R$ {valor:.2f}")

    def sacar(self, valor: float) -> None:
        if valor > self._saldo:
            raise ValueError("Saldo insuficiente")
        self._saldo -= valor
        self._historico.append(f"Saque: R$ {valor:.2f}")

    def extrato(self) -> None:
        print(f"=== Extrato de {self.titular} ===")
        for item in self._historico:
            print(f"  {item}")
        print(f"  Saldo: R$ {self._saldo:.2f}")
2.2 Lista de objetos — turma de alunos
list[Tipo] for busca

Objetivo

Gerenciar uma coleção de objetos. Adicionar, listar e buscar. Esse padrão é a base de qualquer CRUD.

Python — turma.py
class Aluno:
    def __init__(self, nome: str, matricula: str) -> None:
        self.nome = nome
        self.matricula = matricula

    def __str__(self) -> str:
        return f"[{self.matricula}] {self.nome}"


class Turma:
    def __init__(self, nome: str) -> None:
        self.nome = nome
        self.alunos: list[Aluno] = []  # lista começa vazia

    def adicionar(self, aluno: Aluno) -> None:
        self.alunos.append(aluno)

    def listar(self) -> None:
        print(f"Turma: {self.nome}")
        for aluno in self.alunos:
            print(f"  {aluno}")

    def buscar(self, nome: str) -> Aluno | None:
        for aluno in self.alunos:
            if aluno.nome.lower() == nome.lower():
                return aluno
        return None  # não encontrado


turma = Turma("Python Avançado")
turma.adicionar(Aluno("Ana", "2024-001"))
turma.adicionar(Aluno("Bruno", "2024-002"))
turma.listar()

encontrado = turma.buscar("ana")
print(encontrado)  # [2024-001] Ana

Boa prática — tipo de retorno

Aluno | None é Python moderno (3.10+). Significa: retorna um Aluno ou None. Antes disso, usava-se Optional[Aluno] do módulo typing. Sempre anote quando um método pode não retornar nada.

Desafio

Adicione um método remover(nome: str) -> bool que remove o aluno da lista e retorna True se encontrou e removeu, False se não encontrou.

Resposta
    def remover(self, nome: str) -> bool:
        aluno = self.buscar(nome)
        if aluno is None:
            return False
        self.alunos.remove(aluno)
        return True
03
python moderno

Dataclasses, métodos de classe e estáticos

Menos boilerplate, mais clareza. Ferramentas que Python oferece para escrever OOP de forma mais limpa.

Estimativa: 2–3 horas

Conceitos desta parte

Por que dataclasses?

Toda classe que só armazena dados acaba com o mesmo boilerplate: __init__ atribuindo valores, __str__ formatando. @dataclass gera tudo isso. Use quando a classe é principalmente dados.

3.1 Dataclass — Endereço e Contato
@dataclass field() composição

Regra do field(default_factory=list)

Você nunca pode fazer lista: list = [] numa dataclass. O Python compartilharia a mesma lista entre todas as instâncias — bug clássico. Use field(default_factory=list): cada instância recebe sua própria lista nova.

Nunca faça isso

tags: list[str] = [] em dataclasses. Todos os objetos compartilhariam a mesma lista. Use field(default_factory=list) sempre.

Python — contato.py
from dataclasses import dataclass, field


@dataclass
class Endereco:
    rua: str
    cidade: str
    estado: str

    def __str__(self) -> str:
        return f"{self.rua}, {self.cidade} — {self.estado}"


@dataclass
class Contato:
    nome: str
    telefone: str
    endereco: Endereco
    emails: list[str] = field(default_factory=list)

    def adicionar_email(self, email: str) -> None:
        self.emails.append(email)


end = Endereco("Rua das Flores, 42", "São Paulo", "SP")
ct = Contato("Djalma", "11-9999-0000", end)
ct.adicionar_email("djalma@empresa.com")

print(ct.nome)      # Djalma
print(ct.endereco)  # Rua das Flores, 42, São Paulo — SP
print(ct.emails)    # ['djalma@empresa.com']

Composição > Herança

Contato tem um Endereco — isso é composição. É mais flexível que herança na maioria dos casos práticos. Prefira composição quando a relação é "tem um" em vez de "é um".

Desafio

Adicione um @classmethod chamado do_dict que recebe um dicionário e retorna um Contato:

dados = {"nome": "Ana", "telefone": "11-8888", "rua": "Av Brasil", "cidade": "SP", "estado": "SP"}
ct = Contato.do_dict(dados)
Resposta
@classmethod
def do_dict(cls, dados: dict) -> "Contato":
    end = Endereco(dados["rua"], dados["cidade"], dados["estado"])
    return cls(
        nome=dados["nome"],
        telefone=dados["telefone"],
        endereco=end,
    )
3.2 Métodos estáticos e de classe — fábrica de objetos
@staticmethod @classmethod factory pattern

Quando usar cada um

@staticmethod: utilitário que pertence conceitualmente à classe mas não precisa de dados dela. @classmethod: construtores alternativos — formas diferentes de criar o objeto.

Python
from datetime import date


@dataclass
class Pessoa:
    nome: str
    nascimento: date

    @staticmethod
    def validar_nome(nome: str) -> bool:
        # sem self — não depende de nenhuma instância
        return bool(nome.strip()) and len(nome) >= 2

    @classmethod
    def do_texto(cls, texto: str) -> "Pessoa":
        # cria Pessoa a partir de "Nome,YYYY-MM-DD"
        nome, data_str = texto.split(",")
        nascimento = date.fromisoformat(data_str.strip())
        return cls(nome=nome.strip(), nascimento=nascimento)

    def idade(self) -> int:
        hoje = date.today()
        return hoje.year - self.nascimento.year


p = Pessoa.do_texto("Maria, 1990-04-15")
print(p.nome)          # Maria
print(Pessoa.validar_nome(""))  # False

Desafio

Adicione um método is_aniversariante_hoje() -> bool que retorna True se o mês e dia do nascimento são iguais ao de hoje.

Resposta
    def is_aniversariante_hoje(self) -> bool:
        hoje = date.today()
        return (
            self.nascimento.month == hoje.month
            and self.nascimento.day == hoje.day
        )
04
mini sistemas

Menu de terminal e CRUD simples

Juntar tudo em um programa real. Menu interativo, operações CRUD, separação de responsabilidades.

Estimativa: 3–4 horas

Conceitos desta parte

4.1 Agenda de contatos com menu
CRUD menu while True separação

Estrutura de arquivos

Estrutura
agenda/
├── models.py    # Contato
├── sistema.py   # SistemaAgenda
└── main.py      # ponto de entrada

Por que separar em arquivos?

Quando o modelo muda (ex: adicionar um campo), você mexe só em models.py. Quando a lógica do menu muda, mexe em sistema.py. Cada arquivo tem uma única razão para mudar.

models.py
from dataclasses import dataclass


@dataclass
class Contato:
    nome: str
    telefone: str
    email: str

    def __str__(self) -> str:
        return f"{self.nome} | {self.telefone} | {self.email}"
sistema.py
from models import Contato


class SistemaAgenda:
    def __init__(self) -> None:
        self._contatos: list[Contato] = []

    def adicionar(self, nome: str, tel: str, email: str) -> None:
        self._contatos.append(Contato(nome, tel, email))
        print(f"Contato '{nome}' adicionado.")

    def listar(self) -> None:
        if not self._contatos:
            print("Nenhum contato cadastrado.")
            return
        for i, ct in enumerate(self._contatos, 1):
            print(f"  {i}. {ct}")

    def executar(self) -> None:
        while True:
            print("\n[1] Adicionar  [2] Listar  [0] Sair")
            opcao = input("Opção: ").strip()

            if opcao == "1":
                nome  = input("Nome: ")
                tel   = input("Telefone: ")
                email = input("Email: ")
                self.adicionar(nome, tel, email)
            elif opcao == "2":
                self.listar()
            elif opcao == "0":
                print("Encerrando.")
                break
            else:
                print("Opção inválida.")
main.py
from sistema import SistemaAgenda

if __name__ == "__main__":
    SistemaAgenda().executar()

if __name__ == "__main__"

Esse bloco só executa quando você roda main.py diretamente. Se outro arquivo importar main.py, o bloco não executa. Sempre use em pontos de entrada.

Desafio

Implemente a opção [3] Remover: mostra a lista numerada, pede um número e remove o contato na posição correspondente.

Adicionar no menu e criar o método
# dentro de SistemaAgenda: def remover(self, indice: int) -> None: if not (1 <= indice <= len(self._contatos)): print("Índice inválido.") return removido = self._contatos.pop(indice - 1) print(f"'{removido.nome}' removido.") # no menu: elif opcao == "3": self.listar() num = int(input("Número: ")) self.remover(num)
05
projeto final

Sistema de Eventos e Aniversariantes

Construir um sistema real, completo, passo a passo — integrando tudo que foi aprendido.

Estimativa: 4–6 horas

O que o sistema faz

Um programa de terminal para gerenciar eventos (festas, reuniões, etc.) com cadastro de pessoas, locais e verificação de quem faz aniversário na data do evento.

Roteiro — 8 etapas progressivas

Cada etapa é um exercício independente. Complete uma antes de avançar.

  • E1 Classe Pessoa — nome, nascimento, is_aniversariante(data)
  • E2 Classe Local — nome, endereço, capacidade
  • E3 Classe Evento — nome, data, local, lista de convidados
  • E4 Método Evento.adicionar_convidado com validação de capacidade
  • E5 Método Evento.aniversariantes() — retorna quem faz aniversário na data
  • E6 Classe Sistema — gerencia pessoas e eventos
  • E7 Menu interativo completo com todas as operações
  • E8 Relatório final — eventos do mês, aniversariantes, total de convidados
E1 Classe Pessoa
@dataclassdatevalidação
models/pessoa.py
from dataclasses import dataclass
from datetime import date


@dataclass
class Pessoa:
    nome: str
    nascimento: date

    def is_aniversariante(self, data: date) -> bool:
        return (
            self.nascimento.month == data.month
            and self.nascimento.day == data.day
        )

    def idade_em(self, data: date) -> int:
        anos = data.year - self.nascimento.year
        if (data.month, data.day) < (self.nascimento.month, self.nascimento.day):
            anos -= 1
        return anos

    def __str__(self) -> str:
        return f"{self.nome} (nasc. {self.nascimento.strftime('%d/%m/%Y')})"

Desafio

Adicione um @classmethod Pessoa.do_str(texto) que cria uma pessoa a partir de "Nome,DD/MM/YYYY".

Resposta
    @classmethod
    def do_str(cls, texto: str) -> "Pessoa":
        nome, data_str = texto.split(",")
        nascimento = date.strptime(data_str.strip(), "%d/%m/%Y")
        return cls(nome=nome.strip(), nascimento=nascimento)
E3–E5 Classe Evento — convidados e aniversariantes
composiçãolist comprehensionvalidação
models/evento.py
from dataclasses import dataclass, field
from datetime import date
from models.pessoa import Pessoa
from models.local import Local


@dataclass
class Evento:
    nome: str
    data: date
    local: Local
    convidados: list[Pessoa] = field(default_factory=list)

    def adicionar_convidado(self, pessoa: Pessoa) -> None:
        if len(self.convidados) >= self.local.capacidade:
            raise ValueError(f"Local lotado: capacidade {self.local.capacidade}")
        self.convidados.append(pessoa)

    def aniversariantes(self) -> list[Pessoa]:
        # list comprehension: pythônico e legível
        return [p for p in self.convidados if p.is_aniversariante(self.data)]

    def relatorio(self) -> None:
        print(f"\n{'='*40}")
        print(f"Evento: {self.nome}")
        print(f"Data:   {self.data.strftime('%d/%m/%Y')}")
        print(f"Local:  {self.local.nome} (cap. {self.local.capacidade})")
        print(f"Convidados: {len(self.convidados)}")

        aniv = self.aniversariantes()
        if aniv:
            print("\nAniversariantes do dia:")
            for p in aniv:
                anos = p.idade_em(self.data)
                print(f"  🎂 {p.nome} — {anos} anos")

List comprehension

[p for p in lista if condicao] é a forma pythônica de filtrar listas. É equivalente a um for com if e append, mas mais curto e legível. Use para filtragens simples.

E6–E8 Sistema completo — código final integrado
orquestraçãomenumain.py

Estrutura final

Organização
sistema_eventos/
├── main.py
├── sistema.py
└── models/
    ├── __init__.py
    ├── pessoa.py
    ├── local.py
    └── evento.py
sistema.py — orquestrador completo
from datetime import date
from models.pessoa import Pessoa
from models.local import Local
from models.evento import Evento


class SistemaEventos:
    def __init__(self) -> None:
        self._pessoas: list[Pessoa] = []
        self._locais: list[Local] = []
        self._eventos: list[Evento] = []

    # ── helpers de cadastro ──────────────────────────────

    def _cadastrar_pessoa(self) -> None:
        nome  = input("Nome: ")
        nasc  = input("Nascimento (DD/MM/AAAA): ")
        data  = date.strptime(nasc, "%d/%m/%Y")
        self._pessoas.append(Pessoa(nome, data))
        print(f"Pessoa '{nome}' cadastrada.")

    def _cadastrar_local(self) -> None:
        nome = input("Nome do local: ")
        cap  = int(input("Capacidade: "))
        self._locais.append(Local(nome, cap))
        print(f"Local '{nome}' cadastrado.")

    def _criar_evento(self) -> None:
        if not self._locais:
            print("Cadastre um local primeiro.")
            return
        nome  = input("Nome do evento: ")
        dt    = date.strptime(input("Data (DD/MM/AAAA): "), "%d/%m/%Y")
        for i, l in enumerate(self._locais, 1):
            print(f"  {i}. {l.nome}")
        idx   = int(input("Local (número): ")) - 1
        self._eventos.append(Evento(nome, dt, self._locais[idx]))
        print(f"Evento '{nome}' criado.")

    # ── menu principal ────────────────────────────────────

    def executar(self) -> None:
        while True:
            print("\n[1] Pessoa  [2] Local  [3] Evento  [4] Relatório  [0] Sair")
            op = input("Opção: ").strip()
            if   op == "1": self._cadastrar_pessoa()
            elif op == "2": self._cadastrar_local()
            elif op == "3": self._criar_evento()
            elif op == "4":
                for ev in self._eventos:
                    ev.relatorio()
            elif op == "0": break
            else: print("Inválido.")

O que você construiu

Um sistema completo com 4 classes, composição, dataclasses, validações, menu de terminal e separação em arquivos. Isso já é um projeto Python real e bem organizado.

06
evolução

Próximos passos

Como tornar este projeto mais profissional e o que estudar em seguida.

Persistência

Salvar e carregar dados com json ou sqlite3. Os objetos somem quando o programa fecha — isso resolve o problema.

Testes

Aprender pytest para testar cada método isoladamente. Comece testando is_aniversariante e aniversariantes.

Herança

Criar EventoOnline e EventoPresencial como subclasses de Evento. Entender quando herança faz sentido — e quando composição é melhor.

Interfaces / ABC

Usar abc.ABC e @abstractmethod para definir contratos entre classes — base do polimorfismo real.

Exceções próprias

Criar class CapacidadeExcedida(Exception) em vez de lançar ValueError genérico. Mensagens de erro mais claras.

Refatoração

Mover validações para métodos estáticos de validação. Separar a lógica de display da lógica de negócio.

Leitura recomendada

Clean Code (Robert Martin) para princípios gerais. Fluent Python (Luciano Ramalho) para Python profundo. A documentação oficial do Python em docs.python.org é excelente e gratuita.