Apostila de Python — Do Básico ao Avançado para o Mercado de Trabalho
Para quem é: você já conhece o básico de Python e quer chegar ao nível exigido nas vagas de Backend, Dados e Automação/DevOps.
Como usar: cada capítulo tem teoria enxuta, exemplos executáveis e exercícios com soluções. Digite os códigos você mesmo — não copie e cole. Rode, quebre, conserte.
Versão de referência: Python 3.12+
Sumário
Parte I — Fundamentos Sólidos 1. Ambiente profissional de desenvolvimento 2. Revisão essencial (com as pegadinhas que caem em entrevista) 3. Estruturas de dados a fundo 4. Funções: do básico ao avançado 5. Tratamento de erros e exceções 6. Arquivos, JSON, CSV e pathlib
Parte II — Python Avançado 7. Programação orientada a objetos 8. Iteradores, geradores e itertools 9. Decorators e context managers 10. Type hints e tipagem estática 11. Testes automatizados com pytest 12. Concorrência: threads, processos e asyncio
Parte III — Backend/Web 13. HTTP na prática com requests e httpx 14. APIs REST com FastAPI 15. Bancos de dados: SQL, sqlite3 e SQLAlchemy 16. Noções de Django
Parte IV — Dados 17. NumPy 18. Pandas 19. Visualização de dados 20. SQL + Python no dia a dia de dados
Parte V — Automação e DevOps 21. Scripts de linha de comando profissionais 22. Web scraping 23. Logging profissional 24. Automação de tarefas do dia a dia 25. Docker e CI/CD para desenvolvedores Python
Parte VI — Carreira 26. Boas práticas e código limpo 27. Estruturas de dados e algoritmos para entrevistas 28. Checklist final e próximos passos
PARTE I — FUNDAMENTOS SÓLIDOS
Capítulo 1 — Ambiente profissional de desenvolvimento
Em qualquer vaga, espera-se que você saiba isolar dependências e organizar projetos. Isso é o que separa "sei Python" de "trabalho com Python".
1.1 Ambientes virtuais (venv)
Cada projeto deve ter seu próprio ambiente isolado. Nunca instale pacotes no Python do sistema.
# Criar o ambiente (dentro da pasta do projeto)
python -m venv .venv
# Ativar — Linux/macOS
source .venv/bin/activate
# Ativar — Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Instalar dependências
pip install requests
# Congelar as versões usadas
pip freeze > requirements.txt
# Em outra máquina, recriar o ambiente
pip install -r requirements.txt
# Desativar
deactivate
1.2 uv — o gerenciador moderno
uv vem substituindo pip/venv em muitas empresas por ser muito mais rápido e resolver dependências de forma confiável:
# Instalar o uv (uma vez só)
pip install uv
# Criar projeto novo com pyproject.toml
uv init meu-projeto
cd meu-projeto
# Adicionar dependências (cria .venv automaticamente)
uv add requests pandas
# Rodar um script dentro do ambiente
uv run python main.py
1.3 Estrutura de projeto profissional
meu-projeto/
├── .venv/ # ambiente virtual (não vai pro git)
├── .gitignore
├── pyproject.toml # metadados e dependências
├── README.md
├── src/
│ └── meu_projeto/
│ ├── __init__.py
│ ├── main.py
│ └── utils.py
└── tests/
├── __init__.py
└── test_utils.py
.gitignore mínimo para Python:
.venv/
__pycache__/
*.pyc
.pytest_cache/
.env
dist/
*.egg-info/
1.4 Variáveis de ambiente e segredos
Senhas e chaves de API nunca vão no código. Use variáveis de ambiente:
import os
# Lê a variável; se não existir, retorna None
api_key = os.getenv("API_KEY")
# Com valor padrão
db_url = os.getenv("DATABASE_URL", "sqlite:///dev.db")
Na prática usa-se um arquivo .env (que fica fora do git) com a biblioteca python-dotenv:
pip install python-dotenv
# arquivo .env
API_KEY=sk-abc123
DATABASE_URL=postgresql://user:senha@localhost/app
from dotenv import load_dotenv
import os
load_dotenv() # carrega o .env para as variáveis de ambiente
api_key = os.getenv("API_KEY")
Exercícios — Capítulo 1
1.1 Crie um projeto chamado financas com ambiente virtual, instale requests, gere o requirements.txt e escreva o .gitignore.
1.2 Escreva um script que leia NOME_USUARIO de uma variável de ambiente e imprima uma saudação. Se a variável não existir, use "visitante".
Soluções
1.1
mkdir financas && cd financas
python -m venv .venv
source .venv/bin/activate # ou .venv\Scripts\Activate.ps1 no Windows
pip install requests
pip freeze > requirements.txt
printf '.venv/\n__pycache__/\n*.pyc\n.env\n' > .gitignore
1.2
import os
nome = os.getenv("NOME_USUARIO", "visitante")
print(f"Olá, {nome}!")
Teste com: NOME_USUARIO=Ana python script.py (Linux/macOS) ou $env:NOME_USUARIO="Ana"; python script.py (PowerShell).
Capítulo 2 — Revisão essencial (com as pegadinhas de entrevista)
Você já sabe o básico, então este capítulo foca no que costuma derrubar candidatos: mutabilidade, cópias, escopo e comparações.
2.1 Tipos e a diferença crucial: mutável vs. imutável
| Imutáveis | Mutáveis |
|---|---|
int, float, bool |
list |
str |
dict |
tuple |
set |
frozenset, bytes |
bytearray |
Por que isso importa? Porque variáveis em Python são referências a objetos:
a = [1, 2, 3]
b = a # b aponta para A MESMA lista
b.append(4)
print(a) # [1, 2, 3, 4] — surpresa!
# Com imutáveis isso não acontece:
x = "olá"
y = x
y = y + "!" # cria uma NOVA string
print(x) # "olá" — intacta
2.2 Cópia rasa vs. cópia profunda
import copy
original = [[1, 2], [3, 4]]
rasa = original.copy() # ou list(original), ou original[:]
rasa[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] — a lista interna é compartilhada!
profunda = copy.deepcopy(original)
profunda[0].append(777)
print(original) # [[1, 2, 99], [3, 4]] — agora sim, intacta
2.3 A pegadinha clássica: argumento padrão mutável
Esta é a pergunta de entrevista mais comum sobre Python:
# ERRADO — o default é criado UMA vez, na definição da função
def adicionar(item, lista=[]):
lista.append(item)
return lista
print(adicionar(1)) # [1]
print(adicionar(2)) # [1, 2] ← a lista foi reaproveitada!
# CERTO — use None como sentinela
def adicionar(item, lista=None):
if lista is None:
lista = []
lista.append(item)
return lista
print(adicionar(1)) # [1]
print(adicionar(2)) # [2] ✓
2.4 is vs. ==
a = [1, 2]
b = [1, 2]
print(a == b) # True — mesmo VALOR
print(a is b) # False — objetos DIFERENTES na memória
# Regra prática: use `is` apenas com None, True e False
if resultado is None:
...
2.5 Truthiness — o que é considerado falso
São "falsy": False, None, 0, 0.0, "", [], {}, set(), (). Todo o resto é "truthy".
itens = []
if not itens: # forma pythônica de checar lista vazia
print("sem itens")
nome = ""
nome_exibicao = nome or "Anônimo" # "Anônimo" — `or` retorna o 1º truthy
Cuidado com a pegadinha: 0 é falsy, então if quantidade: falha quando quantidade == 0 é um valor válido. Nesses casos, compare explicitamente: if quantidade is not None:.
2.6 Escopo: a regra LEGB
Python resolve nomes na ordem Local → Enclosing → Global → Builtin.
x = "global"
def externa():
x = "enclosing"
def interna():
x = "local"
print(x) # local
interna()
print(x) # enclosing
externa()
print(x) # global
Para modificar variáveis de fora:
contador = 0
def incrementa():
global contador # evite quando possível — dificulta testes
contador += 1
def fabrica_contador():
total = 0
def incrementa():
nonlocal total # modifica a variável da função de fora
total += 1
return total
return incrementa
c = fabrica_contador()
print(c(), c(), c()) # 1 2 3
2.7 Desempacotamento e o operador *
# Desempacotamento básico
x, y = 10, 20
x, y = y, x # troca de valores sem variável temporária
# Com * para "pegar o resto"
primeiro, *meio, ultimo = [1, 2, 3, 4, 5]
print(primeiro, meio, ultimo) # 1 [2, 3, 4] 5
# Em chamadas de função
numeros = [3, 1, 4]
print(*numeros) # equivale a print(3, 1, 4)
# Mesclando dicionários
padrao = {"tema": "claro", "fonte": 12}
usuario = {"fonte": 14}
config = {**padrao, **usuario} # {'tema': 'claro', 'fonte': 14}
2.8 f-strings a fundo
nome = "Ana"
saldo = 1234.5678
print(f"{nome=}") # nome='Ana' — ótimo para debug
print(f"{saldo:.2f}") # 1234.57
print(f"{saldo:>12,.2f}") # ' 1,234.57' — alinhado à direita, 12 casas
print(f"{0.157:.1%}") # 15.7%
print(f"{255:08b}") # 11111111 em binário com zeros à esquerda
from datetime import datetime
agora = datetime.now()
print(f"{agora:%d/%m/%Y %H:%M}") # 05/07/2026 14:30
Exercícios — Capítulo 2
2.1 Sem rodar, preveja a saída:
def f(valores=[]):
valores.append(len(valores))
return valores
print(f())
print(f())
print(f([9]))
print(f())
2.2 Escreva uma função mesclar_configs(*dicts) que receba qualquer quantidade de dicionários e retorne um único dicionário onde os últimos sobrescrevem os primeiros.
2.3 Explique por que o código abaixo imprime [1, 1, 1] e conserte:
linhas = [[0] * 3] * 3
linhas[0][0] = 1
print([linha[0] for linha in linhas])
Soluções
2.1
[0]
[0, 1]
[9, 1]
[0, 1, 2]
A lista default é criada uma vez e compartilhada entre chamadas que não passam argumento. A chamada f([9]) usa outra lista e não afeta a default.
2.2
def mesclar_configs(*dicts):
resultado = {}
for d in dicts:
resultado.update(d) # ou: resultado |= d (Python 3.9+)
return resultado
print(mesclar_configs({"a": 1}, {"b": 2}, {"a": 99})) # {'a': 99, 'b': 2}
2.3 [[0]*3]*3 repete a mesma lista interna 3 vezes — as três linhas são o mesmo objeto. Correção:
linhas = [[0] * 3 for _ in range(3)] # cria 3 listas independentes
Capítulo 3 — Estruturas de dados a fundo
Dominar quando usar cada estrutura (e o custo de cada operação) é cobrado direta ou indiretamente em toda entrevista técnica.
3.1 Listas — e o custo das operações
frutas = ["maçã", "banana", "uva"]
frutas.append("manga") # O(1) — adiciona no fim
frutas.insert(0, "kiwi") # O(n) — desloca tudo!
frutas.pop() # O(1) — remove do fim
frutas.pop(0) # O(n) — desloca tudo!
"uva" in frutas # O(n) — varre a lista
frutas.sort() # ordena in-place
ordenadas = sorted(frutas, reverse=True) # retorna nova lista
frutas.sort(key=len) # ordena pelo tamanho da string
Se você precisa inserir/remover nas duas pontas com frequência, use deque:
from collections import deque
fila = deque([1, 2, 3])
fila.appendleft(0) # O(1)
fila.popleft() # O(1) — perfeito para filas (FIFO)
3.2 List comprehensions (e quando NÃO usar)
quadrados = [n**2 for n in range(10)]
pares = [n for n in range(20) if n % 2 == 0]
# Com if/else no valor (repare: o if/else vem ANTES do for)
rotulos = ["par" if n % 2 == 0 else "ímpar" for n in range(5)]
# Aninhada — achatar matriz
matriz = [[1, 2], [3, 4], [5, 6]]
achatada = [x for linha in matriz for x in linha] # [1, 2, 3, 4, 5, 6]
Regra de ouro: se a comprehension não cabe confortavelmente em uma linha ou tem lógica complexa, use um for normal. Legibilidade vence.
3.3 Dicionários — a estrutura mais importante do Python
Busca, inserção e remoção em O(1) na média. Desde o Python 3.7, mantêm ordem de inserção.
usuario = {"nome": "Ana", "idade": 30}
# Acesso seguro
usuario.get("email") # None (sem explodir)
usuario.get("email", "sem email") # valor padrão
# setdefault: pega o valor ou cria se não existir
usuario.setdefault("tags", []).append("admin")
# Iteração
for chave, valor in usuario.items():
print(chave, valor)
# Dict comprehension
precos = {"arroz": 20, "feijão": 8, "carne": 45}
caros = {nome: p for nome, p in precos.items() if p > 10}
# Inverter um dicionário
invertido = {v: k for k, v in precos.items()}
3.4 collections: Counter e defaultdict
Duas ferramentas que aparecem o tempo todo em código profissional e entrevistas:
from collections import Counter, defaultdict
# Counter — contagem instantânea
palavras = "o rato roeu a roupa do rei o rato fugiu".split()
contagem = Counter(palavras)
print(contagem.most_common(2)) # [('o', 2), ('rato', 2)]
# defaultdict — dicionário com valor padrão automático
agrupados = defaultdict(list)
pessoas = [("TI", "Ana"), ("RH", "Bruno"), ("TI", "Carla")]
for depto, nome in pessoas:
agrupados[depto].append(nome) # sem precisar checar se a chave existe
print(dict(agrupados)) # {'TI': ['Ana', 'Carla'], 'RH': ['Bruno']}
3.5 Sets — quando a pergunta é "está aqui ou não?"
permitidos = {"admin", "editor", "viewer"}
"admin" in permitidos # O(1)! Muito mais rápido que lista para busca
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a & b) # interseção: {3, 4}
print(a | b) # união: {1, 2, 3, 4, 5, 6}
print(a - b) # diferença: {1, 2}
print(a ^ b) # diferença simétrica: {1, 2, 5, 6}
# Uso clássico: remover duplicatas
emails = ["a@x.com", "b@x.com", "a@x.com"]
unicos = list(dict.fromkeys(emails)) # preserva a ordem, ao contrário de set()
3.6 Tuplas e namedtuple
Tuplas são listas imutáveis — use para dados que não devem mudar e como chaves de dicionário:
coordenadas = {(0, 0): "origem", (1, 2): "ponto A"} # tupla como chave ✓
from collections import namedtuple
Ponto = namedtuple("Ponto", ["x", "y"])
p = Ponto(3, 4)
print(p.x, p.y) # 3 4 — legível, imutável e leve
(No capítulo de POO veremos dataclasses, a evolução moderna disso.)
3.7 Qual estrutura usar? Tabela de decisão
| Necessidade | Estrutura |
|---|---|
| Sequência ordenada, acesso por índice | list |
| Mapeamento chave → valor | dict |
| Testar pertencimento rápido / eliminar duplicatas | set |
| Dados imutáveis, chave composta de dict | tuple |
| Fila (inserir/remover nas pontas) | collections.deque |
| Contagem de frequência | collections.Counter |
| Agrupamento por chave | collections.defaultdict |
Exercícios — Capítulo 3
3.1 Dada a lista de vendas [("Ana", 100), ("Bruno", 50), ("Ana", 200), ("Carla", 70), ("Bruno", 30)], produza um dicionário com o total por vendedor.
3.2 Escreva primeira_nao_repetida(s) que retorna o primeiro caractere que aparece exatamente uma vez na string (ou None). Ex.: "aabbcdd" → "c". Busque solução O(n).
3.3 Dadas duas listas de e-mails, clientes_2025 e clientes_2026, encontre: (a) quem está nas duas; (b) quem só estava em 2025 (churn); (c) quem é novo em 2026.
Soluções
3.1
from collections import defaultdict
vendas = [("Ana", 100), ("Bruno", 50), ("Ana", 200), ("Carla", 70), ("Bruno", 30)]
totais = defaultdict(int)
for vendedor, valor in vendas:
totais[vendedor] += valor
print(dict(totais)) # {'Ana': 300, 'Bruno': 80, 'Carla': 70}
3.2
from collections import Counter
def primeira_nao_repetida(s):
contagem = Counter(s) # 1ª passada: O(n)
for ch in s: # 2ª passada: O(n)
if contagem[ch] == 1:
return ch
return None
print(primeira_nao_repetida("aabbcdd")) # c
3.3
c25 = set(clientes_2025)
c26 = set(clientes_2026)
fieis = c25 & c26 # nas duas
churn = c25 - c26 # só em 2025
novos = c26 - c25 # só em 2026
Capítulo 4 — Funções: do básico ao avançado
4.1 Argumentos posicionais, nomeados, args e *kwargs
def relatorio(titulo, *linhas, separador="-", **metadados):
print(titulo)
print(separador * len(titulo))
for linha in linhas:
print(linha)
for chave, valor in metadados.items():
print(f"[{chave}: {valor}]")
relatorio(
"Vendas Q1",
"Janeiro: 100", "Fevereiro: 150",
separador="=",
autor="Ana", versao=2,
)
*linhascaptura os posicionais extras numa tupla.**metadadoscaptura os nomeados extras num dicionário.
Você pode forçar que argumentos sejam nomeados (comum em bibliotecas modernas):
def criar_usuario(nome, *, admin=False, ativo=True):
...
criar_usuario("Ana", admin=True) # ✓
criar_usuario("Ana", True) # ✗ TypeError — tudo após * exige nome
4.2 Funções são objetos de primeira classe
def gritar(texto): return texto.upper() + "!"
def sussurrar(texto): return texto.lower() + "..."
# Funções podem ser guardadas, passadas e retornadas
estilos = {"grito": gritar, "sussurro": sussurrar}
print(estilos["grito"]("olá")) # OLÁ!
# Passadas como argumento (higher-order functions)
nomes = ["ana", "BRUNO", "Carla"]
print(sorted(nomes, key=str.lower)) # ordena ignorando maiúsculas
print(max(["oi", "python", "sim"], key=len)) # 'python'
4.3 Lambdas — pequenas e no lugar certo
produtos = [("arroz", 20), ("feijão", 8), ("carne", 45)]
# Ordenar pelo preço
por_preco = sorted(produtos, key=lambda p: p[1])
# Melhor ainda, sem lambda:
from operator import itemgetter
por_preco = sorted(produtos, key=itemgetter(1))
Use lambda apenas para expressões de uma linha passadas a outras funções. Se precisar de nome ou de mais de uma expressão, escreva um def.
4.4 Closures — funções que "lembram"
def criar_multiplicador(fator):
def multiplicar(x):
return x * fator # `fator` fica capturado na closure
return multiplicar
dobrar = criar_multiplicador(2)
triplicar = criar_multiplicador(3)
print(dobrar(10), triplicar(10)) # 20 30
Closures são a base dos decorators (Capítulo 9).
4.5 Documentando funções: docstrings
def calcular_juros(principal: float, taxa: float, meses: int) -> float:
"""Calcula o montante com juros compostos.
Args:
principal: valor inicial investido.
taxa: taxa mensal em decimal (0.01 = 1%).
meses: quantidade de meses.
Returns:
Montante final.
Raises:
ValueError: se algum argumento for negativo.
"""
if principal < 0 or taxa < 0 or meses < 0:
raise ValueError("argumentos não podem ser negativos")
return principal * (1 + taxa) ** meses
Esse formato (Google style) é padrão em muitas empresas. help(calcular_juros) exibe a docstring.
Exercícios — Capítulo 4
4.1 Escreva aplicar_pipeline(valor, *funcoes) que aplica as funções em sequência: aplicar_pipeline(" Olá ", str.strip, str.lower) → "olá".
4.2 Crie criar_validador(minimo, maximo) que retorna uma função que valida se um número está no intervalo (inclusive).
4.3 Dada a lista pedidos = [{"id": 1, "total": 250}, {"id": 2, "total": 90}, {"id": 3, "total": 400}], use sorted com key para ordenar do maior para o menor total.
Soluções
4.1
def aplicar_pipeline(valor, *funcoes):
for f in funcoes:
valor = f(valor)
return valor
4.2
def criar_validador(minimo, maximo):
def validar(n):
return minimo <= n <= maximo
return validar
valida_idade = criar_validador(0, 120)
print(valida_idade(30), valida_idade(200)) # True False
4.3
ordenados = sorted(pedidos, key=lambda p: p["total"], reverse=True)
Capítulo 5 — Tratamento de erros e exceções
Código profissional não é código sem erros — é código que lida bem com erros.
5.1 try / except / else / finally
def ler_numero(texto):
try:
n = int(texto)
except ValueError:
print(f"'{texto}' não é um número válido")
return None
else:
# roda só se NÃO houve exceção
print("conversão ok")
return n
finally:
# roda SEMPRE — usado para limpeza (fechar arquivos, conexões...)
print("fim da tentativa")
5.2 Capture exceções específicas
# RUIM — engole qualquer erro, inclusive bugs seus
try:
processar()
except Exception:
pass # 🚨 nunca faça isso
# BOM — trata o que você sabe tratar
try:
with open("dados.json") as f:
dados = json.load(f)
except FileNotFoundError:
dados = {} # padrão razoável
except json.JSONDecodeError as e:
raise ValueError(f"arquivo corrompido: {e}") from e
O from e preserva a exceção original no traceback (encadeamento) — detalhe que impressiona em code review.
5.3 Levantando exceções e criando as suas
class SaldoInsuficienteError(Exception):
"""Levantada quando o saque excede o saldo."""
def __init__(self, saldo, valor):
self.saldo = saldo
self.valor = valor
super().__init__(f"saldo {saldo:.2f} insuficiente para sacar {valor:.2f}")
def sacar(saldo, valor):
if valor > saldo:
raise SaldoInsuficienteError(saldo, valor)
return saldo - valor
try:
sacar(100, 500)
except SaldoInsuficienteError as e:
print(e) # saldo 100.00 insuficiente para sacar 500.00
print(e.valor) # 500 — dados estruturados disponíveis
Exceções customizadas dão nomes de domínio aos erros e permitem tratá-los separadamente dos erros genéricos.
5.4 EAFP vs. LBYL
Python favorece o estilo EAFP ("easier to ask forgiveness than permission"):
# LBYL — "olhe antes de pular" (sujeito a condição de corrida)
if "email" in usuario:
enviar(usuario["email"])
# EAFP — pythônico
try:
enviar(usuario["email"])
except KeyError:
pass
Nenhum é sempre certo; EAFP brilha quando a falha é excepcional, LBYL quando é comum.
Exercícios — Capítulo 5
5.1 Escreva dividir_seguro(a, b) que retorne a / b, mas retorne None em divisão por zero e levante TypeError com mensagem clara se os argumentos não forem numéricos.
5.2 Crie a exceção IdadeInvalidaError e uma função validar_idade(idade) que a levante para idades fora de 0–130.
5.3 Refatore para tratar erros adequadamente (arquivo pode não existir, conteúdo pode não ser JSON):
import json
def carregar_config(caminho):
f = open(caminho)
return json.load(f)
Soluções
5.1
def dividir_seguro(a, b):
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError(f"esperados números, recebidos {type(a).__name__} e {type(b).__name__}")
try:
return a / b
except ZeroDivisionError:
return None
5.2
class IdadeInvalidaError(Exception):
pass
def validar_idade(idade):
if not 0 <= idade <= 130:
raise IdadeInvalidaError(f"idade {idade} fora do intervalo 0–130")
return idade
5.3
import json
def carregar_config(caminho):
try:
with open(caminho, encoding="utf-8") as f: # with garante fechamento
return json.load(f)
except FileNotFoundError:
return {}
except json.JSONDecodeError as e:
raise ValueError(f"config inválida em {caminho}: {e}") from e
Capítulo 6 — Arquivos, JSON, CSV e pathlib
Manipular arquivos é o feijão com arroz de automação, dados e backend.
6.1 pathlib — o jeito moderno de lidar com caminhos
from pathlib import Path
pasta = Path("relatorios")
arquivo = pasta / "2026" / "vendas.csv" # / monta caminhos multiplataforma
print(arquivo.name) # vendas.csv
print(arquivo.stem) # vendas
print(arquivo.suffix) # .csv
print(arquivo.parent) # relatorios/2026
arquivo.parent.mkdir(parents=True, exist_ok=True) # cria pastas
print(arquivo.exists())
# Listar todos os .csv recursivamente
for csv in Path(".").rglob("*.csv"):
print(csv, csv.stat().st_size, "bytes")
6.2 Lendo e escrevendo texto
from pathlib import Path
# Jeito rápido para arquivos pequenos
conteudo = Path("notas.txt").read_text(encoding="utf-8")
Path("saida.txt").write_text("olá\n", encoding="utf-8")
# Jeito clássico com with (fecha o arquivo automaticamente)
with open("grande.log", encoding="utf-8") as f:
for linha in f: # itera linha a linha SEM carregar tudo na memória
if "ERROR" in linha:
print(linha.rstrip())
# Modos: "r" ler, "w" sobrescrever, "a" anexar, "rb"/"wb" binário
with open("log.txt", "a", encoding="utf-8") as f:
f.write("nova linha\n")
Sempre especifique encoding="utf-8" — no Windows o padrão pode ser outro e gerar bugs com acentos.
6.3 JSON — o formato universal de APIs
import json
from pathlib import Path
usuario = {"nome": "Ana", "idade": 30, "tags": ["admin", "dev"], "ativo": True}
# Objeto Python → string JSON
texto = json.dumps(usuario, ensure_ascii=False, indent=2)
# String JSON → objeto Python
dados = json.loads('{"a": 1, "b": [true, null]}')
print(dados) # {'a': 1, 'b': [True, None]} ← repare a conversão de tipos
# Direto com arquivos
with open("usuario.json", "w", encoding="utf-8") as f:
json.dump(usuario, f, ensure_ascii=False, indent=2)
with open("usuario.json", encoding="utf-8") as f:
carregado = json.load(f)
Mapa de tipos: dict↔object, list↔array, str↔string, int/float↔number, True↔true, None↔null.
6.4 CSV — planilhas e integrações
import csv
# Escrevendo com DictWriter
funcionarios = [
{"nome": "Ana", "depto": "TI", "salario": 9000},
{"nome": "Bruno", "depto": "RH", "salario": 6500},
]
with open("func.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["nome", "depto", "salario"])
writer.writeheader()
writer.writerows(funcionarios)
# Lendo com DictReader
with open("func.csv", newline="", encoding="utf-8") as f:
for linha in csv.DictReader(f):
print(linha["nome"], float(linha["salario"])) # tudo vem como string!
Detalhes que evitam bugs: newline="" (evita linhas em branco no Windows) e lembrar que todo valor lido é string.
Exercícios — Capítulo 6
6.1 Escreva um script que percorra uma pasta e mova (ou liste) os arquivos por extensão: .jpg → imagens/, .pdf → documentos/, resto → outros/.
6.2 Dado pedidos.csv com colunas cliente,produto,valor, gere resumo.json com o total gasto por cliente.
6.3 Leia um arquivo de log e conte quantas linhas contêm ERROR, WARNING e INFO, sem carregar o arquivo inteiro na memória.
Soluções
6.1
from pathlib import Path
import shutil
DESTINOS = {".jpg": "imagens", ".jpeg": "imagens", ".pdf": "documentos"}
def organizar(pasta):
pasta = Path(pasta)
for arquivo in pasta.iterdir():
if arquivo.is_file():
destino = pasta / DESTINOS.get(arquivo.suffix.lower(), "outros")
destino.mkdir(exist_ok=True)
shutil.move(str(arquivo), destino / arquivo.name)
organizar("downloads")
6.2
import csv, json
from collections import defaultdict
totais = defaultdict(float)
with open("pedidos.csv", newline="", encoding="utf-8") as f:
for linha in csv.DictReader(f):
totais[linha["cliente"]] += float(linha["valor"])
with open("resumo.json", "w", encoding="utf-8") as f:
json.dump(dict(totais), f, ensure_ascii=False, indent=2)
6.3
from collections import Counter
niveis = Counter()
with open("app.log", encoding="utf-8") as f:
for linha in f: # linha a linha: memória constante
for nivel in ("ERROR", "WARNING", "INFO"):
if nivel in linha:
niveis[nivel] += 1
break
print(niveis)
PARTE II — PYTHON AVANÇADO
Capítulo 7 — Programação orientada a objetos
POO é obrigatória para backend (Django/SQLAlchemy são 100% baseados em classes) e muito usada em dados e automação.
7.1 Classes: o essencial bem feito
class ContaBancaria:
# atributo de CLASSE — compartilhado por todas as instâncias
taxa_manutencao = 0.01
def __init__(self, titular, saldo=0):
# atributos de INSTÂNCIA
self.titular = titular
self.saldo = saldo
def depositar(self, valor):
if valor <= 0:
raise ValueError("depósito deve ser positivo")
self.saldo += valor
def sacar(self, valor):
if valor > self.saldo:
raise ValueError("saldo insuficiente")
self.saldo -= valor
conta = ContaBancaria("Ana", 100)
conta.depositar(50)
print(conta.saldo) # 150
7.2 Métodos dunder — integrando com a linguagem
Os métodos __assim__ fazem seus objetos funcionarem com print, ==, len, +, etc.
class Carrinho:
def __init__(self):
self._itens = []
def adicionar(self, produto, preco):
self._itens.append((produto, preco))
def __len__(self): # len(carrinho)
return len(self._itens)
def __contains__(self, produto): # "arroz" in carrinho
return any(p == produto for p, _ in self._itens)
def __iter__(self): # for item in carrinho
return iter(self._itens)
def __repr__(self): # representação para devs (debug)
return f"Carrinho({self._itens!r})"
def __str__(self): # representação para usuários (print)
return f"Carrinho com {len(self)} itens"
c = Carrinho()
c.adicionar("arroz", 20)
print(len(c), "arroz" in c) # 1 True
print(c) # Carrinho com 1 itens
Regra prática: implemente sempre __repr__ (todo objeto merece um repr útil); implemente __eq__ quando igualdade por valor fizer sentido.
class Ponto:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, outro):
if not isinstance(outro, Ponto):
return NotImplemented
return (self.x, self.y) == (outro.x, outro.y)
def __add__(self, outro):
return Ponto(self.x + outro.x, self.y + outro.y)
7.3 dataclasses — 90% dos casos de uso
Quando a classe é principalmente "um pacote de dados", @dataclass gera __init__, __repr__ e __eq__ para você:
from dataclasses import dataclass, field
@dataclass
class Produto:
nome: str
preco: float
tags: list[str] = field(default_factory=list) # NUNCA use lista como default direto
em_estoque: bool = True
def aplicar_desconto(self, pct: float) -> None:
self.preco *= (1 - pct)
p1 = Produto("Teclado", 199.9)
p2 = Produto("Teclado", 199.9)
print(p1) # Produto(nome='Teclado', preco=199.9, tags=[], em_estoque=True)
print(p1 == p2) # True — __eq__ de graça
# Variações úteis:
@dataclass(frozen=True) # imutável — pode ser chave de dict
class Coordenada:
lat: float
lon: float
Em vagas de backend você verá o primo delas o tempo todo: os modelos do Pydantic (Capítulo 14), que adicionam validação.
7.4 Properties — atributos com lógica
class Temperatura:
def __init__(self, celsius):
self.celsius = celsius # passa pelo setter abaixo!
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, valor):
if valor < -273.15:
raise ValueError("abaixo do zero absoluto")
self._celsius = valor
@property
def fahrenheit(self): # atributo calculado, somente leitura
return self._celsius * 9 / 5 + 32
t = Temperatura(25)
print(t.fahrenheit) # 77.0 — sem parênteses, parece atributo
t.celsius = -300 # ValueError
Comece com atributos simples; converta para property quando precisar de validação — a interface externa não muda. Isso é um superpoder do Python.
7.5 Herança e composição
class Funcionario:
def __init__(self, nome, salario):
self.nome = nome
self.salario = salario
def pagamento_mensal(self):
return self.salario
class Vendedor(Funcionario):
def __init__(self, nome, salario, comissao):
super().__init__(nome, salario) # reaproveita o __init__ do pai
self.comissao = comissao
def pagamento_mensal(self): # sobrescrita (override)
return super().pagamento_mensal() + self.comissao
Prefira composição a herança quando a relação não é claramente "é um":
class Motor:
def ligar(self): print("vrum")
class Carro:
def __init__(self):
self.motor = Motor() # Carro TEM um motor (composição)
def ligar(self):
self.motor.ligar()
Hierarquias profundas de herança são um clássico de código difícil de manter — entrevistadores gostam de ouvir isso.
7.6 Classes abstratas e Protocols
from abc import ABC, abstractmethod
class Notificador(ABC):
@abstractmethod
def enviar(self, destinatario: str, mensagem: str) -> None: ...
class NotificadorEmail(Notificador):
def enviar(self, destinatario, mensagem):
print(f"[EMAIL para {destinatario}] {mensagem}")
class NotificadorSMS(Notificador):
def enviar(self, destinatario, mensagem):
print(f"[SMS para {destinatario}] {mensagem}")
def alertar_todos(notificador: Notificador, usuarios: list[str]):
for u in usuarios:
notificador.enviar(u, "Sistema fora do ar!")
alertar_todos(NotificadorSMS(), ["Ana", "Bruno"])
A alternativa moderna é o Protocol (duck typing verificável), que não exige herança:
from typing import Protocol
class TemArea(Protocol):
def area(self) -> float: ...
class Circulo:
def __init__(self, r): self.r = r
def area(self) -> float: return 3.14159 * self.r ** 2
def imprimir_area(forma: TemArea): # aceita QUALQUER classe com .area()
print(f"Área: {forma.area():.2f}")
imprimir_area(Circulo(2)) # funciona sem herdar de nada
7.7 Métodos de classe e estáticos
class Usuario:
def __init__(self, nome, email):
self.nome, self.email = nome, email
@classmethod
def de_dict(cls, dados: dict): # construtor alternativo — padrão comuníssimo
return cls(dados["nome"], dados["email"])
@staticmethod
def email_valido(email: str) -> bool: # função utilitária agrupada na classe
return "@" in email and "." in email.split("@")[-1]
u = Usuario.de_dict({"nome": "Ana", "email": "ana@x.com"})
Exercícios — Capítulo 7
7.1 Modele um sistema de formas geométricas: classe abstrata Forma com método area(); implemente Retangulo e Circulo; escreva maior_forma(formas) que retorna a de maior área.
7.2 Crie a dataclass Tarefa com titulo, prioridade (1–5, validada em __post_init__) e concluida=False. Adicione método concluir().
7.3 Implemente Estoque que se comporte como container: len(estoque) = quantidade total de unidades, "arroz" in estoque funcione, e estoque["arroz"] retorne a quantidade (0 se não existir).
Soluções
7.1
from abc import ABC, abstractmethod
import math
class Forma(ABC):
@abstractmethod
def area(self) -> float: ...
class Retangulo(Forma):
def __init__(self, largura, altura):
self.largura, self.altura = largura, altura
def area(self): return self.largura * self.altura
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
def area(self): return math.pi * self.raio ** 2
def maior_forma(formas):
return max(formas, key=lambda f: f.area())
7.2
from dataclasses import dataclass
@dataclass
class Tarefa:
titulo: str
prioridade: int
concluida: bool = False
def __post_init__(self):
if not 1 <= self.prioridade <= 5:
raise ValueError("prioridade deve estar entre 1 e 5")
def concluir(self):
self.concluida = True
7.3
class Estoque:
def __init__(self):
self._itens = {}
def adicionar(self, produto, qtd):
self._itens[produto] = self._itens.get(produto, 0) + qtd
def __len__(self):
return sum(self._itens.values())
def __contains__(self, produto):
return produto in self._itens
def __getitem__(self, produto):
return self._itens.get(produto, 0)
Capítulo 8 — Iteradores, geradores e itertools
Geradores são a resposta do Python para processar dados grandes com pouca memória — pergunta frequente em vagas de dados e backend.
8.1 O protocolo de iteração
for funciona com qualquer objeto que implemente __iter__ (retorna um iterador) e o iterador implemente __next__:
numeros = [1, 2, 3]
it = iter(numeros) # chama numeros.__iter__()
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# next(it) agora levanta StopIteration — é assim que o for sabe parar
8.2 Geradores com yield
Uma função com yield vira uma fábrica de iteradores preguiçosos: produz um valor por vez, sob demanda.
def contagem_regressiva(n):
print("começando!")
while n > 0:
yield n # pausa aqui e devolve n; retoma na próxima chamada
n -= 1
g = contagem_regressiva(3)
print(next(g)) # começando! \n 3
print(next(g)) # 2
for resto in g: # continua de onde parou
print(resto) # 1
O caso de uso matador — ler arquivos gigantes:
def linhas_com_erro(caminho):
with open(caminho, encoding="utf-8") as f:
for linha in f:
if "ERROR" in linha:
yield linha.strip()
# Processa um log de 10 GB usando alguns KB de memória:
for erro in linhas_com_erro("servidor.log"):
print(erro)
8.3 Generator expressions
Como list comprehensions, mas com parênteses — e sem materializar a lista:
# Lista: cria 1 milhão de números na memória
quadrados_lista = [n**2 for n in range(1_000_000)]
# Gerador: cria um de cada vez
quadrados_gen = (n**2 for n in range(1_000_000))
# Perfeito para agregações:
total = sum(n**2 for n in range(1_000_000))
tem_par = any(n % 2 == 0 for n in [1, 3, 5, 8]) # True — para no primeiro
todos_positivos = all(n > 0 for n in [1, 2, -3]) # False
Regra: se você só vai percorrer uma vez, use gerador; se precisa indexar, reordenar ou percorrer várias vezes, use lista. Atenção: geradores se esgotam após o primeiro uso.
8.4 Encadeando geradores (pipelines)
def ler(caminho):
with open(caminho, encoding="utf-8") as f:
yield from (linha.strip() for linha in f) # yield from delega
def filtrar_erros(linhas):
return (l for l in linhas if "ERROR" in l)
def extrair_timestamp(linhas):
return (l.split(" ")[0] for l in linhas)
# Pipeline lazy: nada roda até o for consumir
pipeline = extrair_timestamp(filtrar_erros(ler("servidor.log")))
for ts in pipeline:
print(ts)
Esse padrão de pipeline é exatamente como ferramentas de ETL funcionam por baixo.
8.5 itertools — o canivete suíço
from itertools import chain, islice, groupby, product, combinations, count
# chain — juntar iteráveis sem criar lista intermediária
for x in chain([1, 2], (3, 4), "ab"):
print(x, end=" ") # 1 2 3 4 a b
# islice — fatiar um gerador (geradores não aceitam [0:5])
primeiros = list(islice(count(10), 5)) # [10, 11, 12, 13, 14]
# groupby — agrupar consecutivos (a entrada precisa estar ORDENADA pela chave!)
vendas = [("TI", 100), ("TI", 200), ("RH", 50)]
for depto, grupo in groupby(vendas, key=lambda v: v[0]):
print(depto, sum(v for _, v in grupo)) # TI 300 / RH 50
# combinações e produto cartesiano
print(list(combinations("ABC", 2))) # [('A','B'), ('A','C'), ('B','C')]
print(list(product([1, 2], "ab"))) # [(1,'a'), (1,'b'), (2,'a'), (2,'b')]
Exercícios — Capítulo 8
8.1 Escreva o gerador fibonacci() (infinito) e use islice para pegar os 10 primeiros números.
8.2 Escreva em_lotes(iteravel, tamanho) que produza listas de até tamanho itens: list(em_lotes(range(7), 3)) → [[0,1,2], [3,4,5], [6]]. (Muito usado para inserir dados em banco em batches.)
8.3 Sem carregar o arquivo inteiro, calcule a média dos valores da coluna valor de um CSV de milhões de linhas, usando gerador.
Soluções
8.1
from itertools import islice
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(islice(fibonacci(), 10))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
8.2
def em_lotes(iteravel, tamanho):
lote = []
for item in iteravel:
lote.append(item)
if len(lote) == tamanho:
yield lote
lote = []
if lote:
yield lote
(No Python 3.12+ existe itertools.batched pronto.)
8.3
import csv
def valores(caminho):
with open(caminho, newline="", encoding="utf-8") as f:
for linha in csv.DictReader(f):
yield float(linha["valor"])
total = 0.0
n = 0
for v in valores("vendas.csv"):
total += v
n += 1
print(total / n if n else 0)
Capítulo 9 — Decorators e context managers
Você já usa decorators (@dataclass, @property) e context managers (with open(...)). Saber criá-los é marca de dev pleno/sênior.
9.1 Anatomia de um decorator
Um decorator é uma função que recebe uma função e retorna outra (geralmente uma versão "embrulhada"):
import functools
import time
def cronometrar(func):
@functools.wraps(func) # preserva nome e docstring da original
def wrapper(*args, **kwargs):
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
duracao = time.perf_counter() - inicio
print(f"{func.__name__} levou {duracao:.4f}s")
return resultado
return wrapper
@cronometrar
def processar():
time.sleep(0.5)
return "ok"
processar() # processar levou 0.5003s
@cronometrar é apenas açúcar sintático para processar = cronometrar(processar).
Nunca esqueça @functools.wraps — sem ele, processar.__name__ viraria "wrapper" e o debugging sofre.
9.2 Decorators com parâmetros
Precisa de uma camada a mais (fábrica de decorators):
import functools, time
def tentar_novamente(vezes=3, espera=1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for tentativa in range(1, vezes + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if tentativa == vezes:
raise
print(f"tentativa {tentativa} falhou ({e}); aguardando {espera}s")
time.sleep(espera)
return wrapper
return decorator
@tentar_novamente(vezes=3, espera=2)
def chamar_api():
...
Esse decorator de retry é praticamente um item de portfólio: aparece em todo sistema que fala com rede.
9.3 Decorators úteis da biblioteca padrão
import functools
@functools.lru_cache(maxsize=None) # memoização automática
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # instantâneo; sem cache seria inviável
@functools.cache # Python 3.9+, equivale a lru_cache(maxsize=None)
def config_pesada():
...
Atenção: só use cache em funções puras (mesma entrada → mesma saída) e com argumentos hasháveis.
9.4 Context managers: criando o seu
O protocolo é __enter__/__exit__, mas o jeito prático é @contextmanager:
from contextlib import contextmanager
import time
@contextmanager
def cronometro(nome):
inicio = time.perf_counter()
try:
yield # aqui roda o corpo do with
finally:
print(f"[{nome}] {time.perf_counter() - inicio:.3f}s")
with cronometro("consulta ao banco"):
time.sleep(0.2)
# [consulta ao banco] 0.200s
Um exemplo do mundo real — transação de banco de dados:
@contextmanager
def transacao(conexao):
try:
yield conexao
conexao.commit()
except Exception:
conexao.rollback()
raise
# uso:
# with transacao(conn) as c:
# c.execute("INSERT ...")
Exercícios — Capítulo 9
9.1 Crie o decorator @logar_chamadas que imprime o nome da função, os argumentos e o valor retornado a cada chamada.
9.2 Crie o decorator com parâmetro @exigir_tipo(int) que valida que todos os argumentos posicionais são do tipo dado, levantando TypeError caso contrário.
9.3 Crie o context manager mudar_diretorio(caminho) que entra numa pasta e garante o retorno à pasta original ao sair, mesmo com exceção.
Soluções
9.1
import functools
def logar_chamadas(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
resultado = func(*args, **kwargs)
args_txt = ", ".join(
[repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
)
print(f"{func.__name__}({args_txt}) -> {resultado!r}")
return resultado
return wrapper
9.2
import functools
def exigir_tipo(tipo):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for a in args:
if not isinstance(a, tipo):
raise TypeError(f"esperado {tipo.__name__}, recebido {type(a).__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@exigir_tipo(int)
def somar(a, b): return a + b
9.3
import os
from contextlib import contextmanager
@contextmanager
def mudar_diretorio(caminho):
original = os.getcwd()
os.chdir(caminho)
try:
yield
finally:
os.chdir(original)
Capítulo 10 — Type hints e tipagem estática
Praticamente toda base de código profissional moderna usa type hints. Elas não mudam a execução — servem para documentação, autocompletar do editor e verificação com mypy/pyright.
10.1 O essencial
def saudacao(nome: str, vezes: int = 1) -> str:
return f"Olá, {nome}! " * vezes
# Coleções (Python 3.9+: use os tipos nativos em minúsculo)
def media(valores: list[float]) -> float: ...
def contagem() -> dict[str, int]: ...
def coordenada() -> tuple[float, float]: ...
# Optional: pode ser o tipo OU None (sintaxe moderna com |)
def buscar_usuario(id: int) -> dict | None: ...
# Union: um de vários tipos
def processar(entrada: str | bytes) -> None: ...
10.2 Tipos mais expressivos
from typing import Any, Callable, Iterable, TypedDict, Literal
# Callable[[tipos_dos_args], tipo_retorno]
def aplicar(f: Callable[[int], int], valores: Iterable[int]) -> list[int]:
return [f(v) for v in valores]
# Literal — restringe a valores específicos
def mover(direcao: Literal["norte", "sul", "leste", "oeste"]) -> None: ...
# TypedDict — dicionários com formato conhecido (comum em respostas de API)
class UsuarioDict(TypedDict):
id: int
nome: str
email: str
def formatar(u: UsuarioDict) -> str:
return f"{u['nome']} <{u['email']}>"
# Aliases de tipo
Matriz = list[list[float]]
def transpor(m: Matriz) -> Matriz: ...
10.3 Generics com TypeVar
from typing import TypeVar
T = TypeVar("T")
def primeiro(itens: list[T]) -> T | None:
return itens[0] if itens else None
x = primeiro([1, 2, 3]) # o verificador infere: int | None
y = primeiro(["a", "b"]) # str | None
(No Python 3.12+ há sintaxe nova: def primeiro[T](itens: list[T]) -> T | None.)
10.4 Verificando com mypy
pip install mypy
mypy meu_modulo.py
def dobro(n: int) -> int:
return n * 2
dobro("olá") # mypy: error: Argument 1 to "dobro" has incompatible type "str"
Em muitas empresas o mypy roda no CI e o merge é bloqueado se houver erros de tipo. Habitue-se a anotar pelo menos as assinaturas de funções públicas.
Exercícios — Capítulo 10
10.1 Anote completamente: uma função que recebe uma lista de dicionários (cada um com nome: str e nota: float) e retorna o nome do melhor aluno ou None se a lista for vazia.
10.2 Escreva filtrar(itens, predicado) genérica e tipada: recebe list[T] e uma função T -> bool, retorna list[T].
Soluções
10.1
from typing import TypedDict
class Aluno(TypedDict):
nome: str
nota: float
def melhor_aluno(alunos: list[Aluno]) -> str | None:
if not alunos:
return None
return max(alunos, key=lambda a: a["nota"])["nome"]
10.2
from typing import Callable, TypeVar
T = TypeVar("T")
def filtrar(itens: list[T], predicado: Callable[[T], bool]) -> list[T]:
return [i for i in itens if predicado(i)]
Capítulo 11 — Testes automatizados com pytest
"Você escreve testes?" é pergunta de toda entrevista. pytest é o padrão de mercado absoluto.
11.1 Primeiro teste
pip install pytest
# src/calculadora.py
def dividir(a: float, b: float) -> float:
if b == 0:
raise ValueError("divisão por zero")
return a / b
# tests/test_calculadora.py
import pytest
from src.calculadora import dividir
def test_divisao_simples():
assert dividir(10, 2) == 5
def test_divisao_por_zero_levanta_erro():
with pytest.raises(ValueError, match="divisão por zero"):
dividir(1, 0)
def test_resultado_float():
assert dividir(1, 3) == pytest.approx(0.333, abs=0.001) # nunca compare floats com ==
pytest # roda tudo
pytest -v # verboso
pytest -k zero # só testes com "zero" no nome
Convenções: arquivos test_*.py, funções test_*. O pytest acha tudo sozinho.
11.2 Parametrização — muitos casos, um teste
import pytest
from src.validadores import email_valido
@pytest.mark.parametrize("email, esperado", [
("ana@empresa.com", True),
("sem-arroba.com", False),
("@sem-nome.com", False),
("ana@sem-ponto", False),
("", False),
])
def test_email_valido(email, esperado):
assert email_valido(email) is esperado
11.3 Fixtures — preparação reutilizável
import pytest
@pytest.fixture
def carrinho_cheio():
from src.loja import Carrinho
c = Carrinho()
c.adicionar("arroz", 20.0)
c.adicionar("feijão", 8.0)
return c
def test_total(carrinho_cheio): # basta pedir pelo nome
assert carrinho_cheio.total() == 28.0
def test_remover(carrinho_cheio): # cada teste recebe um carrinho NOVO
carrinho_cheio.remover("arroz")
assert carrinho_cheio.total() == 8.0
Fixture com limpeza (padrão setup/teardown):
@pytest.fixture
def arquivo_temporario(tmp_path): # tmp_path é fixture nativa do pytest
caminho = tmp_path / "dados.txt"
caminho.write_text("conteúdo de teste")
yield caminho
# código após o yield roda na finalização (aqui tmp_path já se limpa sozinho)
11.4 Mocks — isolando dependências externas
Testes não devem depender de rede, banco ou relógio. Substitua essas partes:
# src/clima.py
import requests
def temperatura_atual(cidade: str) -> float:
resp = requests.get(f"https://api.clima.com/{cidade}")
resp.raise_for_status()
return resp.json()["temp"]
# tests/test_clima.py
from unittest.mock import patch, Mock
from src.clima import temperatura_atual
def test_temperatura_atual():
resposta_falsa = Mock()
resposta_falsa.json.return_value = {"temp": 25.5}
with patch("src.clima.requests.get", return_value=resposta_falsa) as get_falso:
assert temperatura_atual("SP") == 25.5
get_falso.assert_called_once_with("https://api.clima.com/SP")
Detalhe crucial: faça patch de onde a função é usada (src.clima.requests.get), não de onde é definida.
11.5 O que testar (visão de mercado)
- Caminho feliz + casos de borda (vazio, zero, negativo, None).
- Erros esperados (
pytest.raises). - Um bug corrigido = um teste novo que o reproduz.
- Cobertura:
pip install pytest-covepytest --cov=src. Times costumam exigir 80%+, mas cobertura alta com testes ruins não vale nada — teste comportamento, não implementação.
Exercícios — Capítulo 11
11.1 Escreva fizzbuzz(n) (múltiplo de 3 → "Fizz", de 5 → "Buzz", de ambos → "FizzBuzz", senão o número como string) e um teste parametrizado cobrindo os 4 casos.
11.2 Crie uma fixture que gera um CSV temporário com 3 linhas de vendas e um teste que valida a função de soma de valores do Capítulo 8.
11.3 Dada def preco_com_cotacao(valor_usd): return valor_usd * obter_cotacao(), escreva um teste que mocke obter_cotacao para retornar 5.0.
Soluções
11.1
def fizzbuzz(n: int) -> str:
if n % 15 == 0: return "FizzBuzz"
if n % 3 == 0: return "Fizz"
if n % 5 == 0: return "Buzz"
return str(n)
import pytest
@pytest.mark.parametrize("n, esperado", [
(9, "Fizz"), (10, "Buzz"), (30, "FizzBuzz"), (7, "7"),
])
def test_fizzbuzz(n, esperado):
assert fizzbuzz(n) == esperado
11.2
import pytest
@pytest.fixture
def csv_vendas(tmp_path):
arq = tmp_path / "vendas.csv"
arq.write_text("cliente,valor\nAna,100\nBruno,50\nAna,25\n", encoding="utf-8")
return arq
def test_soma_valores(csv_vendas):
from src.relatorios import somar_valores
assert somar_valores(csv_vendas) == 175.0
11.3
from unittest.mock import patch
def test_preco_com_cotacao():
with patch("src.cambio.obter_cotacao", return_value=5.0):
from src.cambio import preco_com_cotacao
assert preco_com_cotacao(10) == 50.0
Capítulo 12 — Concorrência: threads, processos e asyncio
Tema de entrevista para pleno/sênior. A pergunta-chave: seu gargalo é I/O ou CPU?
| Gargalo | Exemplos | Ferramenta |
|---|---|---|
| I/O (espera por rede/disco) | chamar APIs, ler arquivos, consultar banco | threading ou asyncio |
| CPU (cálculo pesado) | processar imagens, cálculos numéricos | multiprocessing |
12.1 O GIL em uma frase
O GIL (Global Interpreter Lock) permite que apenas uma thread execute bytecode Python por vez — por isso threads não aceleram código limitado por CPU, mas funcionam bem para I/O (a thread solta o GIL enquanto espera a rede/disco).
12.2 Threads para I/O com ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
import requests, time
URLS = [f"https://httpbin.org/delay/1" for _ in range(5)]
def baixar(url):
return requests.get(url, timeout=10).status_code
inicio = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as pool:
resultados = list(pool.map(baixar, URLS))
print(resultados, f"{time.perf_counter() - inicio:.1f}s")
# 5 requisições de 1s cada terminam em ~1s em vez de ~5s
Para acompanhar conforme terminam (fora de ordem):
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=5) as pool:
futuros = {pool.submit(baixar, url): url for url in URLS}
for futuro in as_completed(futuros):
url = futuros[futuro]
try:
print(url, "->", futuro.result())
except Exception as e:
print(url, "falhou:", e)
12.3 Processos para CPU
from concurrent.futures import ProcessPoolExecutor
def calcular_pesado(n):
return sum(i * i for i in range(n))
if __name__ == "__main__": # OBRIGATÓRIO com multiprocessing no Windows
with ProcessPoolExecutor() as pool:
resultados = list(pool.map(calcular_pesado, [10**7] * 4))
print(resultados)
# Usa os 4+ núcleos de verdade — o GIL não atrapalha porque são processos separados
12.4 asyncio — concorrência cooperativa
asyncio roda tudo em uma thread, alternando entre tarefas nos pontos de await. É o modelo dos frameworks web modernos (FastAPI!).
import asyncio
import httpx # pip install httpx — o "requests" com suporte async
async def baixar(client: httpx.AsyncClient, url: str) -> int:
resp = await client.get(url) # enquanto espera, outras tarefas rodam
return resp.status_code
async def main():
urls = ["https://httpbin.org/delay/1"] * 5
async with httpx.AsyncClient(timeout=10) as client:
# gather dispara tudo "ao mesmo tempo"
resultados = await asyncio.gather(*(baixar(client, u) for u in urls))
print(resultados)
asyncio.run(main()) # ~1s para as 5 requisições
Regras de ouro do async:
- async def define uma corrotina; chamá-la não executa — precisa de await ou asyncio.run.
- Nunca use bibliotecas bloqueantes (como requests ou time.sleep) dentro de código async — use httpx/asyncio.sleep. Uma chamada bloqueante trava todas as tarefas.
- asyncio.gather(*tarefas) = rode em paralelo e me dê todos os resultados.
12.5 Qual escolher? (resposta de entrevista)
"Se o gargalo é I/O e são poucas tarefas ou código legado síncrono,
ThreadPoolExecutorresolve com simplicidade. Se são milhares de conexões simultâneas ou o framework já é async (FastAPI),asyncio. Se o gargalo é CPU,multiprocessing/ProcessPoolExecutorpara escapar do GIL."
Exercícios — Capítulo 12
12.1 Dada uma lista de 10 URLs, escreva a versão sequencial e a versão com ThreadPoolExecutor de uma função que verifica quais estão no ar (status 200). Compare os tempos.
12.2 Reescreva o exercício 12.1 com asyncio + httpx.
12.3 (Conceitual) Um colega usou threading para acelerar o redimensionamento de 1000 imagens e não viu ganho. Explique o motivo e a solução.
Soluções
12.1
import time, requests
from concurrent.futures import ThreadPoolExecutor
def no_ar(url):
try:
return url, requests.get(url, timeout=5).status_code == 200
except requests.RequestException:
return url, False
def sequencial(urls):
return [no_ar(u) for u in urls]
def paralelo(urls):
with ThreadPoolExecutor(max_workers=10) as pool:
return list(pool.map(no_ar, urls))
# medindo:
# t0 = time.perf_counter(); sequencial(urls); print(time.perf_counter() - t0)
# t0 = time.perf_counter(); paralelo(urls); print(time.perf_counter() - t0)
12.2
import asyncio, httpx
async def no_ar(client, url):
try:
resp = await client.get(url)
return url, resp.status_code == 200
except httpx.HTTPError:
return url, False
async def main(urls):
async with httpx.AsyncClient(timeout=5) as client:
return await asyncio.gather(*(no_ar(client, u) for u in urls))
# asyncio.run(main(urls))
12.3 Redimensionar imagens é trabalho de CPU. Por causa do GIL, as threads revezam o interpretador e não há paralelismo real. Solução: ProcessPoolExecutor, que usa processos separados (cada um com seu GIL) e aproveita todos os núcleos.
PARTE III — BACKEND/WEB
Capítulo 13 — HTTP na prática com requests e httpx
Antes de criar APIs, é preciso saber consumi-las com fluência.
13.1 O vocabulário mínimo de HTTP
- Métodos: GET (ler), POST (criar), PUT/PATCH (atualizar), DELETE (remover).
- Status codes: 2xx sucesso (200 OK, 201 Created, 204 No Content) · 4xx erro do cliente (400 requisição inválida, 401 não autenticado, 403 sem permissão, 404 não existe, 422 dados inválidos) · 5xx erro do servidor.
- Headers: metadados (
Content-Type: application/json,Authorization: Bearer <token>). - Body: o conteúdo, geralmente JSON.
13.2 requests — o essencial profissional
import requests
# GET com query params (?q=python&page=2)
resp = requests.get(
"https://api.github.com/search/repositories",
params={"q": "python", "per_page": 3},
timeout=10, # SEMPRE defina timeout em produção
)
resp.raise_for_status() # levanta exceção em 4xx/5xx
dados = resp.json()
for repo in dados["items"]:
print(repo["full_name"], repo["stargazers_count"])
# POST com JSON
resp = requests.post(
"https://httpbin.org/post",
json={"nome": "Ana", "email": "ana@x.com"}, # json= já serializa e põe o header
headers={"Authorization": "Bearer meu-token"},
timeout=10,
)
# Tratamento de erros completo
try:
resp = requests.get("https://api.exemplo.com/dados", timeout=10)
resp.raise_for_status()
except requests.Timeout:
print("a API demorou demais")
except requests.HTTPError as e:
print(f"erro HTTP {e.response.status_code}")
except requests.RequestException as e: # classe-mãe de todos os erros da lib
print(f"falha de conexão: {e}")
13.3 Sessions, retries e paginação
import requests
from requests.adapters import HTTPAdapter, Retry
def criar_sessao() -> requests.Session:
"""Sessão com retry automático — padrão de produção."""
sessao = requests.Session()
retries = Retry(
total=3,
backoff_factor=1, # espera 1s, 2s, 4s...
status_forcelist=[429, 500, 502, 503, 504],
)
sessao.mount("https://", HTTPAdapter(max_retries=retries))
sessao.headers.update({"User-Agent": "meu-app/1.0"})
return sessao
# Paginação — padrão comum em APIs
def todos_os_itens(sessao, url_base):
pagina = 1
while True:
resp = sessao.get(url_base, params={"page": pagina, "per_page": 100}, timeout=10)
resp.raise_for_status()
itens = resp.json()
if not itens:
break
yield from itens
pagina += 1
Sessions reutilizam a conexão TCP (muito mais rápido para várias chamadas) e centralizam headers/autenticação.
Exercícios — Capítulo 13
13.1 Use a API pública https://pokeapi.co/api/v2/pokemon/{nome} para escrever peso_pokemon(nome) que retorna o peso, tratando 404 com uma mensagem amigável.
13.2 Escreva baixar_todas(urls) que baixa várias URLs com uma Session e retorna {url: status_code}, sem deixar uma falha derrubar as demais.
Soluções
13.1
import requests
def peso_pokemon(nome: str) -> float | None:
resp = requests.get(f"https://pokeapi.co/api/v2/pokemon/{nome.lower()}", timeout=10)
if resp.status_code == 404:
print(f"pokémon '{nome}' não encontrado")
return None
resp.raise_for_status()
return resp.json()["weight"] / 10 # a API retorna em hectogramas
13.2
import requests
def baixar_todas(urls: list[str]) -> dict[str, int | None]:
resultados = {}
with requests.Session() as s:
for url in urls:
try:
resultados[url] = s.get(url, timeout=10).status_code
except requests.RequestException:
resultados[url] = None
return resultados
Capítulo 14 — APIs REST com FastAPI
FastAPI é hoje o framework mais pedido em vagas de backend Python, junto com Django. Ele une async, type hints e validação automática.
14.1 Primeira API
pip install "fastapi[standard]"
# main.py
from fastapi import FastAPI
app = FastAPI(title="API de Tarefas")
@app.get("/")
def raiz():
return {"mensagem": "API no ar!"}
@app.get("/tarefas/{tarefa_id}")
def obter_tarefa(tarefa_id: int, detalhes: bool = False):
# tarefa_id vem do PATH; detalhes vem da QUERY (?detalhes=true)
return {"id": tarefa_id, "detalhes": detalhes}
fastapi dev main.py
# API em http://127.0.0.1:8000
# Documentação interativa AUTOMÁTICA em http://127.0.0.1:8000/docs
Repare: tarefa_id: int faz o FastAPI converter e validar automaticamente. GET /tarefas/abc retorna erro 422 com mensagem clara, sem você escrever uma linha de validação.
14.2 Pydantic — modelos com validação
Pydantic é a espinha dorsal do FastAPI (e aparece sozinho em muitas vagas):
from pydantic import BaseModel, EmailStr, Field
class UsuarioEntrada(BaseModel):
nome: str = Field(min_length=2, max_length=80)
email: EmailStr
idade: int = Field(ge=0, le=130) # ge = greater or equal
tags: list[str] = []
class UsuarioSaida(BaseModel):
id: int
nome: str
email: EmailStr
# repare: sem senha, sem campos internos — modelos de saída filtram dados
u = UsuarioEntrada(nome="Ana", email="ana@x.com", idade=30)
print(u.model_dump()) # → dict
print(u.model_dump_json()) # → string JSON
UsuarioEntrada(nome="A", email="não-é-email", idade=-5)
# ValidationError com a lista exata dos 3 problemas
14.3 CRUD completo (o projeto que caiu na sua entrevista)
# main.py
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI(title="API de Tarefas")
class TarefaEntrada(BaseModel):
titulo: str = Field(min_length=1, max_length=200)
concluida: bool = False
class Tarefa(TarefaEntrada):
id: int
# "Banco" em memória para o exemplo (Capítulo 15 pluga banco de verdade)
banco: dict[int, Tarefa] = {}
proximo_id = 1
@app.post("/tarefas", response_model=Tarefa, status_code=status.HTTP_201_CREATED)
def criar_tarefa(dados: TarefaEntrada):
global proximo_id
tarefa = Tarefa(id=proximo_id, **dados.model_dump())
banco[tarefa.id] = tarefa
proximo_id += 1
return tarefa
@app.get("/tarefas", response_model=list[Tarefa])
def listar_tarefas(concluida: bool | None = None):
tarefas = list(banco.values())
if concluida is not None:
tarefas = [t for t in tarefas if t.concluida == concluida]
return tarefas
@app.get("/tarefas/{tarefa_id}", response_model=Tarefa)
def obter_tarefa(tarefa_id: int):
if tarefa_id not in banco:
raise HTTPException(status_code=404, detail="tarefa não encontrada")
return banco[tarefa_id]
@app.put("/tarefas/{tarefa_id}", response_model=Tarefa)
def atualizar_tarefa(tarefa_id: int, dados: TarefaEntrada):
if tarefa_id not in banco:
raise HTTPException(status_code=404, detail="tarefa não encontrada")
tarefa = Tarefa(id=tarefa_id, **dados.model_dump())
banco[tarefa_id] = tarefa
return tarefa
@app.delete("/tarefas/{tarefa_id}", status_code=status.HTTP_204_NO_CONTENT)
def deletar_tarefa(tarefa_id: int):
if tarefa_id not in banco:
raise HTTPException(status_code=404, detail="tarefa não encontrada")
del banco[tarefa_id]
Teste no /docs ou via curl:
curl -X POST http://127.0.0.1:8000/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo": "Estudar FastAPI"}'
14.4 Injeção de dependências
O sistema de Depends é o coração da arquitetura FastAPI:
from fastapi import Depends, Header, HTTPException
def usuario_atual(authorization: str = Header(default="")) -> str:
"""Extrai e valida o token — reutilizado em qualquer rota."""
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="não autenticado")
token = authorization.removeprefix("Bearer ")
# em produção: validar JWT, buscar usuário no banco...
return token
@app.get("/perfil")
def perfil(usuario: str = Depends(usuario_atual)):
return {"usuario": usuario}
14.5 Endpoints async e testes
import httpx
@app.get("/clima/{cidade}")
async def clima(cidade: str):
async with httpx.AsyncClient() as client:
resp = await client.get(f"https://api.clima.com/{cidade}")
return resp.json()
Use async def quando a rota fizer I/O com bibliotecas async; rotas def normais também funcionam (rodam num threadpool).
Testando a API (isso conta MUITOS pontos em processos seletivos):
# tests/test_api.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_criar_e_buscar_tarefa():
resp = client.post("/tarefas", json={"titulo": "Testar API"})
assert resp.status_code == 201
tarefa_id = resp.json()["id"]
resp = client.get(f"/tarefas/{tarefa_id}")
assert resp.status_code == 200
assert resp.json()["titulo"] == "Testar API"
def test_tarefa_inexistente():
assert client.get("/tarefas/99999").status_code == 404
def test_validacao():
assert client.post("/tarefas", json={"titulo": ""}).status_code == 422
Exercícios — Capítulo 14
14.1 Adicione ao CRUD acima o endpoint PATCH /tarefas/{id}/concluir que marca a tarefa como concluída.
14.2 Crie o modelo Pydantic Produto com nome (3–100 chars), preco (> 0) e sku (exatamente 8 caracteres) e um endpoint POST que o receba.
14.3 Escreva os testes do endpoint do 14.1, cobrindo sucesso e 404.
Soluções
14.1
@app.patch("/tarefas/{tarefa_id}/concluir", response_model=Tarefa)
def concluir_tarefa(tarefa_id: int):
if tarefa_id not in banco:
raise HTTPException(status_code=404, detail="tarefa não encontrada")
banco[tarefa_id].concluida = True
return banco[tarefa_id]
14.2
from pydantic import BaseModel, Field
class Produto(BaseModel):
nome: str = Field(min_length=3, max_length=100)
preco: float = Field(gt=0)
sku: str = Field(min_length=8, max_length=8)
@app.post("/produtos", status_code=201)
def criar_produto(produto: Produto):
return produto
14.3
def test_concluir_tarefa():
tarefa_id = client.post("/tarefas", json={"titulo": "X"}).json()["id"]
resp = client.patch(f"/tarefas/{tarefa_id}/concluir")
assert resp.status_code == 200
assert resp.json()["concluida"] is True
def test_concluir_inexistente():
assert client.patch("/tarefas/99999/concluir").status_code == 404
Capítulo 15 — Bancos de dados: SQL, sqlite3 e SQLAlchemy
Nenhuma vaga de backend ou dados dispensa SQL. Aqui você aprende o SQL essencial e como usá-lo do Python.
15.1 SQL essencial em 15 minutos
-- Criar tabela
CREATE TABLE clientes (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
cidade TEXT,
criado_em TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Inserir
INSERT INTO clientes (nome, cidade) VALUES ('Ana', 'São Paulo');
-- Consultar
SELECT nome, cidade FROM clientes WHERE cidade = 'São Paulo' ORDER BY nome LIMIT 10;
-- Agregações
SELECT cidade, COUNT(*) AS total
FROM clientes
GROUP BY cidade
HAVING COUNT(*) > 5;
-- JOIN (a pergunta de SQL nº 1 em entrevistas)
SELECT c.nome, p.valor
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.valor > 100;
-- Atualizar e deletar (SEMPRE com WHERE!)
UPDATE clientes SET cidade = 'Campinas' WHERE id = 3;
DELETE FROM clientes WHERE id = 3;
Tipos de JOIN: INNER JOIN (só correspondências), LEFT JOIN (tudo da esquerda + correspondências da direita, com NULL onde não há).
15.2 sqlite3 — banco embutido no Python
import sqlite3
con = sqlite3.connect("loja.db")
con.row_factory = sqlite3.Row # permite acessar colunas por nome
cur = con.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL
)
""")
# ⚠️ SEMPRE use placeholders (?) — NUNCA f-string (SQL injection!)
cur.execute("INSERT INTO produtos (nome, preco) VALUES (?, ?)", ("Teclado", 199.9))
cur.executemany(
"INSERT INTO produtos (nome, preco) VALUES (?, ?)",
[("Mouse", 89.9), ("Monitor", 1299.0)],
)
con.commit()
for linha in cur.execute("SELECT * FROM produtos WHERE preco < ?", (500,)):
print(linha["nome"], linha["preco"])
con.close()
O exemplo clássico do desastre (saiba explicar em entrevista):
nome = "x'; DROP TABLE produtos; --"
cur.execute(f"SELECT * FROM produtos WHERE nome = '{nome}'") # 💥 SQL injection
cur.execute("SELECT * FROM produtos WHERE nome = ?", (nome,)) # ✓ seguro
15.3 SQLAlchemy — o ORM padrão do mercado
pip install sqlalchemy
from sqlalchemy import create_engine, ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase):
pass
class Cliente(Base):
__tablename__ = "clientes"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str] = mapped_column(String(80))
email: Mapped[str] = mapped_column(String(120), unique=True)
pedidos: Mapped[list["Pedido"]] = relationship(back_populates="cliente")
class Pedido(Base):
__tablename__ = "pedidos"
id: Mapped[int] = mapped_column(primary_key=True)
valor: Mapped[float]
cliente_id: Mapped[int] = mapped_column(ForeignKey("clientes.id"))
cliente: Mapped[Cliente] = relationship(back_populates="pedidos")
engine = create_engine("sqlite:///loja.db", echo=False) # echo=True mostra o SQL gerado
Base.metadata.create_all(engine)
CRUD com o ORM:
from sqlalchemy import select
# CREATE
with Session(engine) as session:
ana = Cliente(nome="Ana", email="ana@x.com")
ana.pedidos = [Pedido(valor=250.0), Pedido(valor=90.0)]
session.add(ana)
session.commit()
# READ
with Session(engine) as session:
stmt = select(Cliente).where(Cliente.nome.like("A%")).order_by(Cliente.nome)
for cliente in session.scalars(stmt):
print(cliente.nome, [p.valor for p in cliente.pedidos])
# UPDATE
with Session(engine) as session:
cliente = session.scalar(select(Cliente).where(Cliente.email == "ana@x.com"))
cliente.nome = "Ana Silva"
session.commit()
# DELETE
with Session(engine) as session:
cliente = session.get(Cliente, 1) # busca por PK
session.delete(cliente)
session.commit()
Para trocar SQLite por PostgreSQL (produção), basta mudar a URL: create_engine("postgresql+psycopg://user:senha@localhost/loja").
15.4 FastAPI + SQLAlchemy (o combo das vagas)
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
app = FastAPI()
def get_session():
"""Dependência: uma sessão por requisição, sempre fechada no fim."""
with Session(engine) as session:
yield session
class ClienteEntrada(BaseModel):
nome: str
email: str
class ClienteSaida(ClienteEntrada):
id: int
model_config = {"from_attributes": True} # permite converter do objeto ORM
@app.post("/clientes", response_model=ClienteSaida, status_code=201)
def criar_cliente(dados: ClienteEntrada, session: Session = Depends(get_session)):
cliente = Cliente(**dados.model_dump())
session.add(cliente)
session.commit()
session.refresh(cliente) # carrega o id gerado
return cliente
@app.get("/clientes/{cliente_id}", response_model=ClienteSaida)
def obter_cliente(cliente_id: int, session: Session = Depends(get_session)):
cliente = session.get(Cliente, cliente_id)
if cliente is None:
raise HTTPException(404, "cliente não encontrado")
return cliente
Em produção real, acrescente Alembic para migrations (versionamento do schema do banco) — só o nome já vale mencionar em entrevistas.
Exercícios — Capítulo 15
15.1 Em SQL puro: dada a tabela pedidos(id, cliente_id, valor, data), escreva a query que retorna os 5 clientes que mais gastaram.
15.2 Com sqlite3, escreva buscar_por_cidade(cidade) protegida contra SQL injection.
15.3 Modele com SQLAlchemy Autor (1) → Livro (N) e escreva a consulta que lista os livros de um autor pelo nome.
Soluções
15.1
SELECT cliente_id, SUM(valor) AS total
FROM pedidos
GROUP BY cliente_id
ORDER BY total DESC
LIMIT 5;
15.2
def buscar_por_cidade(cidade: str) -> list:
con = sqlite3.connect("loja.db")
con.row_factory = sqlite3.Row
try:
cur = con.execute("SELECT * FROM clientes WHERE cidade = ?", (cidade,))
return [dict(l) for l in cur.fetchall()]
finally:
con.close()
15.3
class Autor(Base):
__tablename__ = "autores"
id: Mapped[int] = mapped_column(primary_key=True)
nome: Mapped[str]
livros: Mapped[list["Livro"]] = relationship(back_populates="autor")
class Livro(Base):
__tablename__ = "livros"
id: Mapped[int] = mapped_column(primary_key=True)
titulo: Mapped[str]
autor_id: Mapped[int] = mapped_column(ForeignKey("autores.id"))
autor: Mapped[Autor] = relationship(back_populates="livros")
from sqlalchemy import select
with Session(engine) as session:
stmt = select(Livro).join(Autor).where(Autor.nome == "Machado de Assis")
livros = session.scalars(stmt).all()
Capítulo 16 — Noções de Django
Django domina vagas de sistemas maiores (e-commerce, plataformas, governo). Você não precisa dominá-lo já, mas precisa entender a arquitetura para conversar sobre ela.
16.1 A filosofia: "baterias inclusas"
Enquanto FastAPI é minimalista, Django traz tudo pronto: ORM próprio, painel admin automático, autenticação, templates, migrations, proteção CSRF.
pip install django
django-admin startproject config .
python manage.py startapp tarefas
python manage.py runserver
16.2 O ciclo Model → Migration → Admin → View → URL
# tarefas/models.py
from django.db import models
class Tarefa(models.Model):
titulo = models.CharField(max_length=200)
concluida = models.BooleanField(default=False)
criada_em = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.titulo
python manage.py makemigrations # gera a migration a partir do model
python manage.py migrate # aplica no banco
python manage.py createsuperuser # cria admin
# tarefas/admin.py — painel de administração COMPLETO em 2 linhas
from django.contrib import admin
from .models import Tarefa
admin.site.register(Tarefa)
# tarefas/views.py
from django.http import JsonResponse
from .models import Tarefa
def listar_tarefas(request):
tarefas = Tarefa.objects.filter(concluida=False).values("id", "titulo")
return JsonResponse(list(tarefas), safe=False)
# config/urls.py
from django.contrib import admin
from django.urls import path
from tarefas.views import listar_tarefas
urlpatterns = [
path("admin/", admin.site.urls),
path("tarefas/", listar_tarefas),
]
16.3 O ORM do Django em 60 segundos
Tarefa.objects.create(titulo="Estudar Django")
Tarefa.objects.all()
Tarefa.objects.filter(concluida=False).order_by("-criada_em")
Tarefa.objects.get(id=1) # levanta exceção se não existir
Tarefa.objects.filter(titulo__icontains="estud") # __icontains, __gte, __in...
tarefa.delete()
16.4 FastAPI ou Django? (resposta de entrevista)
"Django para produtos com muitas funcionalidades clássicas (admin, auth, formulários) onde produtividade inicial importa; FastAPI para APIs e microsserviços, especialmente com alta concorrência de I/O ou integração com ML. Nas empresas grandes é comum os dois convivendo."
Para APIs REST em Django, o padrão de mercado é o Django REST Framework (DRF) — vale conhecer o nome e o conceito de serializers (o análogo dos modelos Pydantic).
PARTE IV — DADOS
Capítulo 17 — NumPy
NumPy é a fundação de todo o ecossistema de dados (pandas, scikit-learn, PyTorch). O conceito central: vetorização — operar em arrays inteiros sem for.
pip install numpy
17.1 Arrays e vetorização
import numpy as np
precos = np.array([100.0, 250.0, 80.0, 320.0])
# Operações elemento a elemento — sem loop!
com_imposto = precos * 1.1
descontos = precos - 10
print(com_imposto) # [110. 275. 88. 352.]
# Comparação de velocidade (a razão de existir do NumPy):
grande = np.arange(10_000_000)
# %timeit [x * 2 for x in grande] → ~600 ms (loop Python)
# %timeit grande * 2 → ~10 ms (vetorizado, em C)
17.2 Criação, formato e indexação
np.zeros((3, 4)) # matriz 3x4 de zeros
np.ones(5)
np.arange(0, 10, 2) # [0 2 4 6 8]
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ]
np.random.default_rng(42).normal(size=(2, 3)) # aleatórios reprodutíveis
m = np.arange(12).reshape(3, 4)
print(m.shape) # (3, 4)
print(m[0, 2]) # elemento linha 0, coluna 2
print(m[1]) # linha inteira
print(m[:, 1]) # coluna inteira
print(m[0:2, 1:3]) # submatriz
17.3 Máscaras booleanas — o padrão mais usado
notas = np.array([7.5, 4.0, 9.2, 5.5, 8.0])
aprovados = notas >= 6 # array de True/False
print(notas[aprovados]) # [7.5 9.2 8. ] — filtra!
print(notas[notas < 6]) # [4. 5.5]
# Combinando condições: & (e), | (ou) — com parênteses obrigatórios
meio = notas[(notas >= 5) & (notas < 8)]
# Substituição condicional
ajustadas = np.where(notas < 6, 6.0, notas) # se < 6 vira 6, senão mantém
17.4 Agregações e eixos
vendas = np.array([
[100, 200, 150], # loja A: jan, fev, mar
[80, 120, 300], # loja B
])
print(vendas.sum()) # 950 — tudo
print(vendas.sum(axis=0)) # [180 320 450] — soma por COLUNA (mês)
print(vendas.sum(axis=1)) # [450 500] — soma por LINHA (loja)
print(vendas.mean(), vendas.max(), vendas.std())
print(vendas.argmax()) # índice do maior valor
Memorize: axis=0 colapsa as linhas (resultado por coluna); axis=1 colapsa as colunas (resultado por linha).
Exercícios — Capítulo 17
17.1 Gere 1000 alturas simuladas com rng.normal(170, 10, size=1000) e calcule: média, desvio padrão e percentual de pessoas acima de 185 cm.
17.2 Dada a matriz de notas (alunos x provas), calcule a média de cada aluno e liste os índices dos aprovados (média ≥ 6).
Soluções
17.1
import numpy as np
rng = np.random.default_rng(42)
alturas = rng.normal(170, 10, size=1000)
print(alturas.mean(), alturas.std())
print((alturas > 185).mean() * 100, "%") # média de booleanos = proporção
17.2
notas = np.array([[7, 8, 6], [4, 5, 5], [9, 9, 10]])
medias = notas.mean(axis=1)
aprovados = np.where(medias >= 6)[0]
print(medias, aprovados) # [7. 4.67 9.33] [0 2]
Capítulo 18 — Pandas
Pandas é a ferramenta de vagas de dados no Brasil e no mundo. Domine DataFrame, filtros, groupby e merge e você cobre 80% do dia a dia.
pip install pandas
18.1 Series e DataFrame
import pandas as pd
df = pd.DataFrame({
"nome": ["Ana", "Bruno", "Carla", "Diego", "Elisa"],
"depto": ["TI", "RH", "TI", "Vendas", "TI"],
"salario": [9000, 6500, 11000, 7000, 8500],
"idade": [30, 45, 28, 38, 33],
})
df.head() # primeiras linhas
df.info() # tipos e nulos — SEMPRE o primeiro comando num dataset novo
df.describe() # estatísticas das colunas numéricas
df.shape # (5, 4)
df["salario"] # uma coluna (Series)
df[["nome", "salario"]] # várias colunas (DataFrame)
18.2 Filtros (boolean indexing)
df[df["salario"] > 8000]
df[(df["depto"] == "TI") & (df["idade"] < 35)] # & e | com parênteses
df[df["depto"].isin(["TI", "RH"])]
df[df["nome"].str.startswith("A")]
# query — alternativa legível
df.query("salario > 8000 and depto == 'TI'")
18.3 loc, iloc e criação de colunas
df.loc[0, "nome"] # por rótulo: linha 0, coluna nome
df.iloc[0, 0] # por posição: primeira linha, primeira coluna
df.loc[df["depto"] == "TI", ["nome", "salario"]] # filtro + seleção
# Novas colunas — vetorizado
df["salario_anual"] = df["salario"] * 13
df["senior"] = df["idade"] >= 35
# Coluna condicional
import numpy as np
df["faixa"] = np.where(df["salario"] >= 9000, "alta", "padrão")
# Modificação com loc (evita o famoso SettingWithCopyWarning)
df.loc[df["depto"] == "RH", "salario"] *= 1.05
18.4 groupby — a operação nº 1 das entrevistas de dados
# Média salarial por departamento
df.groupby("depto")["salario"].mean()
# Várias agregações de uma vez
resumo = df.groupby("depto").agg(
salario_medio=("salario", "mean"),
maior_salario=("salario", "max"),
qtd_pessoas=("nome", "count"),
).reset_index()
# Agrupar por mais de uma coluna
df.groupby(["depto", "senior"])["salario"].mean()
O modelo mental: split (divide pelos grupos) → apply (agrega cada grupo) → combine (junta o resultado).
18.5 Dados sujos: nulos, duplicatas e tipos
O trabalho real de dados é 80% limpeza:
df.isna().sum() # quantos nulos por coluna
df = df.dropna(subset=["salario"]) # remove linhas com salário nulo
df["idade"] = df["idade"].fillna(df["idade"].median())
df = df.drop_duplicates(subset=["nome"], keep="first")
# Tipos errados são a praga dos CSVs
df["salario"] = pd.to_numeric(df["salario"], errors="coerce") # inválido vira NaN
df["data"] = pd.to_datetime(df["data"], format="%d/%m/%Y", errors="coerce")
df["nome"] = df["nome"].str.strip().str.title()
df = df.rename(columns={"depto": "departamento"})
18.6 merge — os JOINs do pandas
funcionarios = pd.DataFrame({
"id": [1, 2, 3, 4],
"nome": ["Ana", "Bruno", "Carla", "Diego"],
"depto_id": [10, 20, 10, 30],
})
departamentos = pd.DataFrame({
"depto_id": [10, 20, 99],
"depto": ["TI", "RH", "Jurídico"],
})
pd.merge(funcionarios, departamentos, on="depto_id", how="inner") # só correspondências
pd.merge(funcionarios, departamentos, on="depto_id", how="left") # todos os funcionários
# how="right", how="outer" completam o quarteto — mesmos conceitos do SQL
# Empilhar DataFrames com as mesmas colunas
pd.concat([df_jan, df_fev], ignore_index=True)
18.7 Ler e gravar arquivos
df = pd.read_csv("vendas.csv", sep=";", decimal=",", encoding="utf-8")
df = pd.read_excel("planilha.xlsx", sheet_name="2026") # pip install openpyxl
df = pd.read_json("dados.json")
df = pd.read_sql("SELECT * FROM vendas", con=engine) # direto do banco!
df.to_csv("saida.csv", index=False)
df.to_excel("saida.xlsx", index=False)
df.to_parquet("saida.parquet") # formato colunar, padrão em data lakes
sep=";" e decimal="," salvam vidas com CSVs brasileiros.
18.8 Séries temporais e pivot (bônus frequente)
vendas = pd.DataFrame({
"data": pd.to_datetime(["2026-01-05", "2026-01-20", "2026-02-03", "2026-02-15"]),
"loja": ["A", "B", "A", "B"],
"valor": [100, 150, 200, 120],
})
vendas["mes"] = vendas["data"].dt.to_period("M")
vendas.groupby("mes")["valor"].sum()
# Pivot: linhas viram tabela cruzada
vendas.pivot_table(index="mes", columns="loja", values="valor", aggfunc="sum")
# loja A loja B
# 2026-01 100 150
# 2026-02 200 120
Exercícios — Capítulo 18
18.1 Com o DataFrame de funcionários da seção 18.1: (a) filtre TI com salário acima da média geral; (b) adicione a coluna bonus = 10% do salário para seniores e 5% para os demais.
18.2 Dado um CSV de pedidos com cliente, produto, quantidade, preco_unitario, calcule o faturamento total por cliente, do maior para o menor.
18.3 Você recebeu df_vendas (id_vendedor, valor) e df_vendedores (id_vendedor, nome, regiao). Alguns vendedores não têm vendas. Produza o total vendido por região, incluindo regiões com total 0.
Soluções
18.1
media = df["salario"].mean()
ti_acima = df[(df["depto"] == "TI") & (df["salario"] > media)]
import numpy as np
df["bonus"] = np.where(df["idade"] >= 35, df["salario"] * 0.10, df["salario"] * 0.05)
18.2
df = pd.read_csv("pedidos.csv")
df["faturamento"] = df["quantidade"] * df["preco_unitario"]
resultado = (
df.groupby("cliente")["faturamento"]
.sum()
.sort_values(ascending=False)
.reset_index()
)
18.3
completo = pd.merge(df_vendedores, df_vendas, on="id_vendedor", how="left")
completo["valor"] = completo["valor"].fillna(0)
por_regiao = completo.groupby("regiao")["valor"].sum().reset_index()
O how="left" a partir de vendedores garante que todos apareçam; o fillna(0) zera quem não vendeu.
Capítulo 19 — Visualização de dados
Comunicar resultados é metade do trabalho de dados. matplotlib é a base; conheça também seaborn e plotly de nome.
pip install matplotlib
19.1 Os quatro gráficos que resolvem 90% dos casos
import matplotlib.pyplot as plt
import numpy as np
fig, eixos = plt.subplots(2, 2, figsize=(12, 8))
# 1. Linha — evolução no tempo
meses = ["Jan", "Fev", "Mar", "Abr", "Mai"]
vendas = [100, 150, 130, 210, 260]
eixos[0, 0].plot(meses, vendas, marker="o")
eixos[0, 0].set_title("Vendas por mês")
# 2. Barras — comparação entre categorias
eixos[0, 1].bar(["TI", "RH", "Vendas"], [9500, 6500, 7000], color="steelblue")
eixos[0, 1].set_title("Salário médio por depto")
# 3. Histograma — distribuição
rng = np.random.default_rng(42)
eixos[1, 0].hist(rng.normal(170, 10, 1000), bins=30, edgecolor="black")
eixos[1, 0].set_title("Distribuição de alturas")
# 4. Dispersão — relação entre variáveis
x = rng.uniform(0, 10, 100)
eixos[1, 1].scatter(x, 2 * x + rng.normal(0, 2, 100), alpha=0.6)
eixos[1, 1].set_title("Relação x vs y")
fig.tight_layout()
fig.savefig("painel.png", dpi=150)
plt.show()
19.2 Direto do pandas
df.groupby("depto")["salario"].mean().plot(kind="bar", title="Salário médio")
df["idade"].plot(kind="hist", bins=10)
vendas_mensais.plot(kind="line", marker="o")
plt.tight_layout()
plt.savefig("grafico.png")
19.3 Boas práticas de visualização
- Todo gráfico tem título e eixos rotulados (
set_xlabel,set_ylabel). - Barras para categorias, linhas para tempo, histograma para distribuição, dispersão para correlação.
- Evite pizza com mais de 3–4 fatias; barras horizontais comunicam melhor.
- Salve com
dpi=150+ para relatórios.
Exercício — Capítulo 19
19.1 Com o DataFrame de vendas do exercício 18.2, gere um gráfico de barras horizontais com o top 5 clientes por faturamento, com título e rótulos.
Solução
top5 = resultado.head(5).sort_values("faturamento") # ascendente p/ barh ficar certo
fig, ax = plt.subplots(figsize=(8, 4))
ax.barh(top5["cliente"], top5["faturamento"], color="seagreen")
ax.set_title("Top 5 clientes por faturamento")
ax.set_xlabel("Faturamento (R$)")
fig.tight_layout()
fig.savefig("top5.png", dpi=150)
Capítulo 20 — SQL + Python no dia a dia de dados
Em vagas de dados, o fluxo típico é: extrair com SQL → transformar com pandas → carregar/reportar. É o famoso ETL.
20.1 pandas + SQLAlchemy
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine("sqlite:///loja.db")
# Extrair: o filtro pesado fica no BANCO (mais eficiente)
df = pd.read_sql(
"""
SELECT c.nome AS cliente, p.valor, p.data
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.data >= '2026-01-01'
""",
con=engine,
)
# Transformar: regras de negócio no pandas
df["data"] = pd.to_datetime(df["data"])
df["mes"] = df["data"].dt.to_period("M")
resumo = df.pivot_table(index="cliente", columns="mes", values="valor", aggfunc="sum").fillna(0)
# Carregar: de volta ao banco ou para arquivo
resumo.to_sql("resumo_mensal", con=engine, if_exists="replace")
resumo.to_excel("resumo_mensal.xlsx")
20.2 Onde fazer cada coisa? (pergunta clássica)
Filtros, joins e agregações em milhões de linhas: no SQL (o banco é otimizado para isso e você trafega menos dados). Lógica de negócio complexa, limpeza de strings, integração com APIs e estatística: no pandas. Errar essa divisão — puxar a tabela inteira para filtrar no pandas — é red flag em entrevista.
20.3 Esqueleto de um ETL de verdade
import logging
import pandas as pd
from sqlalchemy import create_engine
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
def extrair(engine) -> pd.DataFrame:
log.info("extraindo pedidos...")
return pd.read_sql("SELECT * FROM pedidos WHERE data >= date('now', '-30 days')", engine)
def transformar(df: pd.DataFrame) -> pd.DataFrame:
log.info("transformando %d linhas...", len(df))
df = df.dropna(subset=["valor"])
df = df[df["valor"] > 0]
df["valor"] = df["valor"].round(2)
return df
def carregar(df: pd.DataFrame, engine) -> None:
log.info("carregando %d linhas...", len(df))
df.to_sql("pedidos_limpos", engine, if_exists="replace", index=False)
def main():
engine = create_engine("sqlite:///loja.db")
carregar(transformar(extrair(engine)), engine)
log.info("ETL concluído ✓")
if __name__ == "__main__":
main()
Separar em funções puras torna cada etapa testável com pytest — mencione isso e ganhe pontos.
Exercício — Capítulo 20
20.1 Escreva um ETL que leia vendas.csv (colunas: data, vendedor, valor com valores sujos: nulos e negativos), limpe, calcule o total por vendedor e grave o resultado numa tabela SQLite ranking_vendedores.
Solução
import pandas as pd
from sqlalchemy import create_engine
def main():
df = pd.read_csv("vendas.csv")
df["valor"] = pd.to_numeric(df["valor"], errors="coerce")
df = df.dropna(subset=["valor", "vendedor"])
df = df[df["valor"] > 0]
ranking = (
df.groupby("vendedor")["valor"].sum()
.sort_values(ascending=False)
.reset_index()
.rename(columns={"valor": "total"})
)
engine = create_engine("sqlite:///vendas.db")
ranking.to_sql("ranking_vendedores", engine, if_exists="replace", index=False)
if __name__ == "__main__":
main()
PARTE V — AUTOMAÇÃO E DEVOPS
Capítulo 21 — Scripts de linha de comando profissionais
A diferença entre um script "de estudante" e um profissional: aceita argumentos, tem ajuda embutida, retorna códigos de saída corretos e loga o que faz.
21.1 argparse
# organizador.py
import argparse
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="Organiza arquivos por extensão.")
parser.add_argument("pasta", type=Path, help="pasta a organizar")
parser.add_argument("--simular", action="store_true",
help="mostra o que seria feito, sem mover nada")
parser.add_argument("--extensoes", nargs="*", default=[".jpg", ".pdf"],
help="extensões a considerar")
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
if not args.pasta.is_dir():
parser.error(f"{args.pasta} não é uma pasta válida") # sai com código 2
for arquivo in args.pasta.iterdir():
if arquivo.suffix.lower() in args.extensoes:
if args.verbose or args.simular:
print(f"{'[SIMULAÇÃO] ' if args.simular else ''}movendo {arquivo.name}")
if not args.simular:
... # mover de fato
if __name__ == "__main__":
main()
python organizador.py downloads --simular -v
python organizador.py --help # ajuda gerada automaticamente
O padrão --simular (dry run) é marca de quem já quebrou produção uma vez e aprendeu.
21.2 Códigos de saída e stdin/stdout
import sys
def main() -> int:
dados = sys.stdin.read() # permite: cat arquivo.txt | python script.py
if not dados.strip():
print("erro: entrada vazia", file=sys.stderr) # erros no stderr!
return 1 # ≠ 0 sinaliza falha para o shell/CI
print(dados.upper()) # resultado no stdout
return 0
if __name__ == "__main__":
sys.exit(main())
Isso permite compor seu script com pipes e usá-lo em pipelines de CI.
21.3 Executando outros programas: subprocess
import subprocess
resultado = subprocess.run(
["git", "status", "--short"], # lista de argumentos — nunca string com shell=True
capture_output=True,
text=True,
check=True, # levanta CalledProcessError se o comando falhar
timeout=30,
)
print(resultado.stdout)
Evite shell=True com dados vindos do usuário — é a versão shell do SQL injection.
Exercício — Capítulo 21
21.1 Escreva um CLI contador.py caminho --palavra ERRO que conta ocorrências da palavra num arquivo, com --ignorar-caso, e retorna código de saída 1 se o arquivo não existir.
Solução
import argparse, sys
from pathlib import Path
def main() -> int:
parser = argparse.ArgumentParser(description="Conta ocorrências de uma palavra.")
parser.add_argument("caminho", type=Path)
parser.add_argument("--palavra", required=True)
parser.add_argument("--ignorar-caso", action="store_true")
args = parser.parse_args()
if not args.caminho.is_file():
print(f"erro: {args.caminho} não existe", file=sys.stderr)
return 1
texto = args.caminho.read_text(encoding="utf-8")
palavra = args.palavra
if args.ignorar_caso:
texto, palavra = texto.lower(), palavra.lower()
print(texto.count(palavra))
return 0
if __name__ == "__main__":
sys.exit(main())
Capítulo 22 — Web scraping
Coleta de dados da web aparece em vagas de dados, automação e até backend (integrações sem API).
pip install requests beautifulsoup4
22.1 requests + BeautifulSoup
import requests
from bs4 import BeautifulSoup
resp = requests.get(
"https://quotes.toscrape.com/", # site feito para treinar scraping
headers={"User-Agent": "Mozilla/5.0 (estudo)"},
timeout=10,
)
resp.raise_for_status()
sopa = BeautifulSoup(resp.text, "html.parser")
for citacao in sopa.select("div.quote"): # seletores CSS
texto = citacao.select_one("span.text").get_text(strip=True)
autor = citacao.select_one("small.author").get_text(strip=True)
tags = [t.get_text() for t in citacao.select("a.tag")]
print(f"{texto} — {autor} {tags}")
Seletores CSS essenciais: div.classe, #id, tag[attr="valor"], pai > filho.
22.2 Paginação e educação com o servidor
import time
import requests
from bs4 import BeautifulSoup
def raspar_todas_as_paginas():
url = "https://quotes.toscrape.com/"
with requests.Session() as s:
s.headers["User-Agent"] = "Mozilla/5.0 (estudo)"
while url:
resp = s.get(url, timeout=10)
resp.raise_for_status()
sopa = BeautifulSoup(resp.text, "html.parser")
for q in sopa.select("div.quote span.text"):
yield q.get_text(strip=True)
proximo = sopa.select_one("li.next > a")
url = (
"https://quotes.toscrape.com" + proximo["href"] if proximo else None
)
time.sleep(1) # pausa entre requisições — respeite o servidor
Boas práticas inegociáveis: verifique o robots.txt e os termos do site, identifique-se no User-Agent, faça pausas, e prefira sempre a API oficial se existir. Sites com JavaScript pesado exigem outras ferramentas — Playwright ou Selenium (saiba os nomes).
Exercício — Capítulo 22
22.1 Raspe https://quotes.toscrape.com/ e grave um CSV citacoes.csv com colunas texto, autor das 3 primeiras páginas.
Solução
import csv, time, requests
from bs4 import BeautifulSoup
with open("citacoes.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["texto", "autor"])
with requests.Session() as s:
for pagina in range(1, 4):
resp = s.get(f"https://quotes.toscrape.com/page/{pagina}/", timeout=10)
resp.raise_for_status()
sopa = BeautifulSoup(resp.text, "html.parser")
for q in sopa.select("div.quote"):
writer.writerow([
q.select_one("span.text").get_text(strip=True),
q.select_one("small.author").get_text(strip=True),
])
time.sleep(1)
Capítulo 23 — Logging profissional
print para depurar é aceitável; print em produção, não. Logging dá níveis, timestamps, destinos configuráveis e é o que os recrutadores esperam ver no seu código.
23.1 Setup padrão
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
handlers=[
logging.StreamHandler(), # console
logging.FileHandler("app.log", encoding="utf-8"), # arquivo
],
)
log = logging.getLogger(__name__) # um logger por módulo — padrão universal
log.debug("detalhe de depuração") # não aparece (nível INFO)
log.info("processamento iniciado")
log.warning("arquivo de config ausente, usando padrão")
log.error("falha ao conectar no banco")
log.critical("sistema em estado irrecuperável")
Níveis: DEBUG < INFO < WARNING < ERROR < CRITICAL. Em produção roda-se INFO ou WARNING; em desenvolvimento, DEBUG.
23.2 Logando exceções do jeito certo
try:
processar_pagamento(pedido)
except PagamentoError:
log.exception("falha no pagamento do pedido %s", pedido.id)
# log.exception = log.error + traceback completo automático
Repare no estilo %s com argumentos separados (lazy formatting) — a string só é montada se o log for realmente emitido.
23.3 Rotação de arquivos
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
"app.log", maxBytes=5_000_000, backupCount=3, encoding="utf-8"
)
# quando app.log atinge 5 MB → vira app.log.1, e assim por diante (máx. 3 backups)
Sem rotação, seu log de produção um dia enche o disco — história real de todo time de ops.
Capítulo 24 — Automação de tarefas do dia a dia
Combinações práticas que aparecem em vagas de automação/RPA e nos testes técnicos.
24.1 Datas e horários (essencial para qualquer automação)
from datetime import datetime, date, timedelta
from zoneinfo import ZoneInfo
agora = datetime.now(ZoneInfo("America/Sao_Paulo")) # sempre com timezone!
hoje = date.today()
ontem = hoje - timedelta(days=1)
em_30_dias = hoje + timedelta(days=30)
# String → data (parse) e data → string (format)
vencimento = datetime.strptime("15/08/2026", "%d/%m/%Y")
print(vencimento.strftime("%Y-%m-%d")) # 2026-08-15
# Diferença entre datas
delta = vencimento.date() - hoje
print(f"faltam {delta.days} dias")
# Códigos mais usados: %d dia, %m mês, %Y ano, %H hora, %M min, %S seg
24.2 Enviando e-mails
import smtplib
from email.message import EmailMessage
import os
def enviar_relatorio(destinatario: str, anexo: str):
msg = EmailMessage()
msg["Subject"] = "Relatório diário"
msg["From"] = os.environ["EMAIL_USER"]
msg["To"] = destinatario
msg.set_content("Segue o relatório em anexo.")
with open(anexo, "rb") as f:
msg.add_attachment(
f.read(), maintype="application", subtype="pdf",
filename=os.path.basename(anexo),
)
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
smtp.login(os.environ["EMAIL_USER"], os.environ["EMAIL_PASS"])
smtp.send_message(msg)
(Para Gmail, use uma "senha de app". E lembre: credenciais em variáveis de ambiente, nunca no código.)
24.3 Agendamento
Opção 1 — o agendador do sistema chama seu script (padrão profissional): cron no Linux (0 8 * * * /usr/bin/python /app/relatorio.py) ou Task Scheduler no Windows.
Opção 2 — a biblioteca schedule para scripts que ficam rodando:
import schedule, time
def gerar_relatorio():
print("gerando relatório...")
schedule.every().day.at("08:00").do(gerar_relatorio)
schedule.every(30).minutes.do(lambda: print("ping"))
while True:
schedule.run_pending()
time.sleep(1)
Em vagas de dados, o nome a conhecer é Airflow (orquestrador de pipelines em Python) — cite-o quando falarem de agendamento de ETL.
24.4 Um robô completo: monitor de preço
Juntando requests + logging + e-mail + agendamento:
import logging, os, requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("monitor")
LIMITE = 4000.0
def preco_atual() -> float:
resp = requests.get("https://api.loja.com/produto/123", timeout=10)
resp.raise_for_status()
return resp.json()["preco"]
def main():
try:
preco = preco_atual()
log.info("preço atual: R$ %.2f", preco)
if preco < LIMITE:
log.info("abaixo do limite! notificando...")
# enviar_email(...) / mensagem no Slack / etc.
except requests.RequestException:
log.exception("falha ao consultar o preço")
if __name__ == "__main__":
main() # agendado no cron a cada hora
Exercício — Capítulo 24
24.1 Escreva um script que liste todos os arquivos de uma pasta modificados nos últimos 7 dias, com nome, tamanho em KB e data de modificação formatada, logando o resultado.
Solução
import logging
from datetime import datetime, timedelta
from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger(__name__)
def arquivos_recentes(pasta: str, dias: int = 7):
limite = datetime.now() - timedelta(days=dias)
for arq in Path(pasta).iterdir():
if arq.is_file():
modificado = datetime.fromtimestamp(arq.stat().st_mtime)
if modificado >= limite:
yield arq, modificado
for arq, modificado in arquivos_recentes("."):
log.info(
"%-30s %8.1f KB %s",
arq.name, arq.stat().st_size / 1024, modificado.strftime("%d/%m/%Y %H:%M"),
)
Capítulo 25 — Docker e CI/CD para desenvolvedores Python
Você não precisa ser DevOps, mas "sabe containerizar sua aplicação?" é pergunta padrão de vaga backend/dados.
25.1 Dockerfile para uma app Python
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Copiar só o requirements primeiro aproveita o cache de camadas:
# se o código mudar mas as deps não, o pip install não roda de novo
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["fastapi", "run", "main.py", "--port", "8000"]
docker build -t minha-api .
docker run -p 8000:8000 --env-file .env minha-api
Conceitos para saber explicar: imagem (o pacote imutável), container (a instância rodando), camadas/cache (por que copiamos o requirements primeiro), -p (mapeia porta do host para o container).
25.2 docker-compose — app + banco juntos
# compose.yaml
services:
api:
build: .
ports: ["8000:8000"]
environment:
DATABASE_URL: postgresql+psycopg://app:senha@db/loja
depends_on: [db]
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: senha
POSTGRES_DB: loja
volumes:
- dados_pg:/var/lib/postgresql/data
volumes:
dados_pg:
docker compose up --build # sobe API + Postgres com um comando
25.3 CI com GitHub Actions
O pipeline mínimo que toda vaga espera: a cada push, rodar lint e testes automaticamente.
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
testes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Instalar dependências
run: |
pip install -r requirements.txt
pip install pytest ruff
- name: Lint
run: ruff check .
- name: Testes
run: pytest -v
Com isso, um PR com teste quebrado fica vermelho e não pode ser mesclado. É o coração da cultura de qualidade — e o ruff (linter/formatador ultrarrápido) virou o padrão do ecossistema.
Exercício — Capítulo 25
25.1 Escreva o Dockerfile para a API de tarefas do Capítulo 14 e o comando para rodá-la na porta 9000 do seu computador.
Solução
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["fastapi", "run", "main.py", "--port", "8000"]
docker build -t api-tarefas .
docker run -p 9000:8000 api-tarefas
# API acessível em http://localhost:9000/docs
PARTE VI — CARREIRA
Capítulo 26 — Boas práticas e código limpo
O que separa candidatos em testes técnicos raramente é "funcionar" — é a qualidade do código.
26.1 PEP 8 automatizado
Não decore regras de estilo; automatize:
pip install ruff
ruff check . # aponta problemas (imports não usados, variáveis mortas...)
ruff format . # formata o código no padrão
O essencial do estilo: snake_case para funções e variáveis, PascalCase para classes, MAIUSCULAS para constantes, 4 espaços de indentação.
26.2 Nomes que contam a história
# Ruim
def calc(d, t):
return d / t
# Bom
def velocidade_media(distancia_km: float, tempo_horas: float) -> float:
return distancia_km / tempo_horas
Regras práticas: funções são verbos (calcular_total, enviar_email), booleanos soam como pergunta (esta_ativo, tem_permissao), nada de data2, aux, temp, x (fora de loops curtos).
26.3 Funções pequenas, guard clauses e constantes
# Evite pirâmides de if — inverta as condições e saia cedo (guard clauses)
def processar_pedido(pedido):
if pedido is None:
raise ValueError("pedido vazio")
if not pedido.itens:
raise ValueError("pedido sem itens")
if pedido.pago:
return "já processado"
# caminho feliz, sem indentação profunda
...
# Números mágicos viram constantes nomeadas
DIAS_PARA_VENCIMENTO = 30
TAXA_JUROS_MENSAL = 0.02
Se uma função precisa da conjunção "e" para ser descrita ("valida E salva E notifica"), divida-a.
26.4 O Zen do Python aplicado
import this # imprime o Zen do Python
Os que mais importam no dia a dia: explícito é melhor que implícito; simples é melhor que complexo; legibilidade conta; erros nunca devem passar silenciosamente.
26.5 Git — o mínimo profissional
git checkout -b feature/relatorio-mensal # trabalhe em branches
git add -p # revise o que está commitando
git commit -m "Adiciona exportação do relatório mensal em PDF"
git push origin feature/relatorio-mensal # e abra um Pull Request
Commits pequenos e com mensagens no imperativo ("Adiciona", "Corrige", "Remove"), um assunto por PR, e nunca commitar .env, senhas ou a pasta .venv.
26.6 Checklist de code review (use no seu próprio código)
- [ ] Nomes claros e consistentes?
- [ ] Funções pequenas com uma responsabilidade?
- [ ] Erros tratados (e não engolidos)?
- [ ] Sem código morto ou comentado?
- [ ] Sem segredos hardcoded?
- [ ] Tem testes para o comportamento novo?
- [ ]
ruff checkpassa limpo?
Capítulo 27 — Estruturas de dados e algoritmos para entrevistas
Os padrões abaixo cobrem a grande maioria das perguntas de nível júnior/pleno em processos com Python.
27.1 Big-O em 5 linhas
- O(1): acesso a dict/set, append em lista.
- O(log n): busca binária.
- O(n): varrer uma lista.
- O(n log n): ordenação (
sorted). - O(n²): loop dentro de loop — quase sempre dá para melhorar com dict/set.
A jogada que resolve metade das questões: trocar busca em lista (O(n)) por busca em dict/set (O(1)).
27.2 Padrão 1 — dict como índice (Two Sum)
Dada uma lista e um alvo, retorne os índices de dois números que somam o alvo.
def two_sum(numeros: list[int], alvo: int) -> tuple[int, int] | None:
vistos = {} # valor -> índice
for i, n in enumerate(numeros):
complemento = alvo - n
if complemento in vistos: # O(1)!
return vistos[complemento], i
vistos[n] = i
return None
print(two_sum([2, 7, 11, 15], 9)) # (0, 1)
Versão ingênua: dois loops, O(n²). Com dict: O(n). Saber narrar essa melhoria É a entrevista.
27.3 Padrão 2 — Counter (anagramas, frequências)
from collections import Counter
def sao_anagramas(a: str, b: str) -> bool:
return Counter(a.replace(" ", "").lower()) == Counter(b.replace(" ", "").lower())
def mais_frequente(itens: list) -> object:
return Counter(itens).most_common(1)[0][0]
27.4 Padrão 3 — dois ponteiros
Verifique se uma string é palíndromo considerando só letras.
def eh_palindromo(s: str) -> bool:
s = [c.lower() for c in s if c.isalnum()]
esq, dir = 0, len(s) - 1
while esq < dir:
if s[esq] != s[dir]:
return False
esq += 1
dir -= 1
return True
print(eh_palindromo("A man, a plan, a canal: Panama")) # True
27.5 Padrão 4 — janela deslizante
Maior soma de uma subsequência contígua de k elementos.
def maior_soma_janela(nums: list[int], k: int) -> int:
soma = sum(nums[:k])
melhor = soma
for i in range(k, len(nums)):
soma += nums[i] - nums[i - k] # entra um, sai um: O(n) total
melhor = max(melhor, soma)
return melhor
27.6 Padrão 5 — busca binária
def busca_binaria(ordenados: list[int], alvo: int) -> int:
esq, dir = 0, len(ordenados) - 1
while esq <= dir:
meio = (esq + dir) // 2
if ordenados[meio] == alvo:
return meio
if ordenados[meio] < alvo:
esq = meio + 1
else:
dir = meio - 1
return -1
(No mundo real, use o módulo bisect — mas saiba escrever na mão.)
27.7 Padrão 6 — pilha (parênteses balanceados)
def balanceado(s: str) -> bool:
pares = {")": "(", "]": "[", "}": "{"}
pilha = []
for c in s:
if c in "([{":
pilha.append(c)
elif c in pares:
if not pilha or pilha.pop() != pares[c]:
return False
return not pilha
print(balanceado("({[]})"), balanceado("(]")) # True False
27.8 Padrão 7 — recursão e memoização
from functools import cache
@cache
def formas_de_subir(degraus: int) -> int:
"""De quantas formas subir uma escada dando passos de 1 ou 2 degraus?"""
if degraus <= 1:
return 1
return formas_de_subir(degraus - 1) + formas_de_subir(degraus - 2)
27.9 Como se portar na entrevista técnica
- Repita o problema com suas palavras e confirme casos de borda (vazio? negativo? duplicado?).
- Proponha a solução simples primeiro e diga sua complexidade.
- Melhore ("posso trocar essa busca por um set e ir de O(n²) para O(n)").
- Escreva o código falando o raciocínio em voz alta.
- Teste mentalmente com um exemplo pequeno antes de dizer "pronto".
Exercícios — Capítulo 27
27.1 duplicados(nums) → lista dos elementos que aparecem mais de uma vez, em O(n).
27.2 primeiro_faltante(nums) → dado [3, 4, -1, 1], retorne o menor inteiro positivo ausente (aqui, 2).
27.3 inverter_palavras("o rato roeu") → "roeu rato o" sem usar reversed/[::-1] na lista de palavras (faça com pilha ou dois ponteiros).
Soluções
27.1
from collections import Counter
def duplicados(nums: list) -> list:
return [v for v, c in Counter(nums).items() if c > 1]
27.2
def primeiro_faltante(nums: list[int]) -> int:
existentes = set(nums) # O(n) espaço, buscas O(1)
i = 1
while i in existentes:
i += 1
return i
27.3
def inverter_palavras(frase: str) -> str:
pilha = frase.split()
resultado = []
while pilha:
resultado.append(pilha.pop()) # pilha: último a entrar, primeiro a sair
return " ".join(resultado)
Capítulo 28 — Checklist final e próximos passos
28.1 Autoavaliação por área
Base (todas as vagas) - [ ] Explico mutabilidade e a pegadinha do default mutável - [ ] Escolho a estrutura certa (list/dict/set/tuple) justificando pelo custo - [ ] Escrevo comprehensions, geradores, decorators e context managers - [ ] Uso POO com dataclasses, properties e classes abstratas - [ ] Anoto tipos e rodo mypy/ruff sem erros - [ ] Escrevo testes pytest com fixtures, parametrização e mocks - [ ] Sei quando usar threads, processos ou asyncio (e explico o GIL)
Backend - [ ] Construo um CRUD completo em FastAPI com Pydantic e testes - [ ] Escrevo SQL com JOIN e GROUP BY e uso SQLAlchemy com sessões - [ ] Explico status codes, injeção de dependência e por que placeholders evitam SQL injection
Dados - [ ] Limpo dados reais no pandas (nulos, tipos, duplicatas) - [ ] Resolvo groupby, merge e pivot sem consultar a documentação - [ ] Divido corretamente o trabalho entre SQL e pandas - [ ] Monto um ETL com logging e funções testáveis
Automação/DevOps - [ ] Escrevo CLIs com argparse, dry-run e códigos de saída - [ ] Faço scraping educado com requests + BeautifulSoup - [ ] Uso logging com níveis e rotação em vez de print - [ ] Containerizo uma app com Dockerfile e monto um CI com lint + testes
28.2 Três projetos de portfólio (um por área)
- API de controle financeiro — FastAPI + SQLAlchemy + PostgreSQL, autenticação por token, testes com 80%+ de cobertura, Docker e CI no GitHub Actions.
- Pipeline de dados públicos — baixar dados abertos (ex.: portal da transparência), limpar com pandas, carregar em SQLite/Postgres, gerar relatório com gráficos; agendável e com logging.
- Robô de monitoramento — CLI que vigia preços/sites/APIs, com retries, logging rotativo, notificação por e-mail e dry-run.
Cada projeto com README explicando decisões, prints e instruções de execução — recrutadores leem READMEs, não código.
28.3 Para seguir estudando
- Documentação oficial (docs.python.org) — o tutorial oficial e a referência da stdlib são excelentes.
- Livros: Fluent Python (Luciano Ramalho) para aprofundar a linguagem; Architecture Patterns with Python para backend.
- Prática de algoritmos: resolva problemas fáceis/médios com constância (20–30 problemas bem entendidos valem mais que 200 decorados).
- Leia código bom: explore o código-fonte de bibliotecas como FastAPI e requests no GitHub.
28.4 A regra de ouro
Você não aprende Python lendo esta apostila — aprende quebrando e consertando os códigos dela. Digite, rode, mude uma linha, preveja o resultado, confira. Erro seu + correção sua = conhecimento que fica.
Bons estudos e boa sorte nas entrevistas! 🐍
Apostila gerada em julho/2026. Versões de bibliotecas evoluem — em caso de divergência, a documentação oficial manda.