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.
str, int, list)if / else)for / while)def, return)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.
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".
class — define um molde__init__ — inicializa o objeto quando ele é criadoself — referência ao próprio objetoself 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.
Criar a classe mais simples possível. Entender o que __init__ faz e por que existe.
__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.
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.
Adicione um atributo idade: int e um método info() que retorna uma string no formato:
"Rex (Labrador, 3 anos)"
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)
__str__ — o objeto se apresenta
__str__ existeQuando 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.
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.
Crie uma classe Livro com titulo, autor e ano. Implemente __str__ para retornar:
"Dom Casmurro — Machado de Assis (1899)"
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)
list[Tipo])_atributo (convenção de "privado")@propertyEsconder 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.
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.
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.
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.
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}")
Gerenciar uma coleção de objetos. Adicionar, listar e buscar. Esse padrão é a base de qualquer CRUD.
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.
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.
def remover(self, nome: str) -> bool: aluno = self.buscar(nome) if aluno is None: return False self.alunos.remove(aluno) return True
@dataclass — gera __init__, __str__, __repr__ automaticamentefield(default_factory=list) — listas padrão em dataclasses@staticmethod — função utilitária dentro da classe, sem acesso a self@classmethod — método alternativo de construçãoToda 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.
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.
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".
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)
@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, )
@staticmethod: utilitário que pertence conceitualmente à classe mas não precisa de dados dela. @classmethod: construtores alternativos — formas diferentes de criar o objeto.
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
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.
def is_aniversariante_hoje(self) -> bool: hoje = date.today() return ( self.nascimento.month == hoje.month and self.nascimento.day == hoje.day )
while True e input()Sistema como orquestradoraagenda/ ├── models.py # Contato ├── sistema.py # SistemaAgenda └── main.py # ponto de entrada
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.
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}"
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.")
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.
Implemente a opção [3] Remover: mostra a lista numerada, pede um número e remove o contato na posição correspondente.
# 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)
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.
Cada etapa é um exercício independente. Complete uma antes de avançar.
Pessoa — nome, nascimento, is_aniversariante(data)Local — nome, endereço, capacidadeEvento — nome, data, local, lista de convidadosEvento.adicionar_convidado com validação de capacidadeEvento.aniversariantes() — retorna quem faz aniversário na dataSistema — gerencia pessoas e eventosfrom 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')})"
Adicione um @classmethod Pessoa.do_str(texto) que cria uma pessoa a partir de "Nome,DD/MM/YYYY".
@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)
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.
sistema_eventos/
├── main.py
├── sistema.py
└── models/
├── __init__.py
├── pessoa.py
├── local.py
└── evento.py
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.
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.