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,
)
  • *linhas captura os posicionais extras numa tupla.
  • **metadados captura 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: .jpgimagens/, .pdfdocumentos/, 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-cov e pytest --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, ThreadPoolExecutor resolve com simplicidade. Se são milhares de conexões simultâneas ou o framework já é async (FastAPI), asyncio. Se o gargalo é CPU, multiprocessing/ProcessPoolExecutor para 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 check passa 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

  1. Repita o problema com suas palavras e confirme casos de borda (vazio? negativo? duplicado?).
  2. Proponha a solução simples primeiro e diga sua complexidade.
  3. Melhore ("posso trocar essa busca por um set e ir de O(n²) para O(n)").
  4. Escreva o código falando o raciocínio em voz alta.
  5. 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)

  1. API de controle financeiro — FastAPI + SQLAlchemy + PostgreSQL, autenticação por token, testes com 80%+ de cobertura, Docker e CI no GitHub Actions.
  2. 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.
  3. 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.