Backend

Python

Referência completa de Python moderno cobrindo tipos, funções, classes, async, generics, testes, packaging e evolução da linguagem

Tipos básicos: int, float, str, bool, bytes, None — conversões e built-ins

Python é dinamicamente tipado mas fortemente tipado — você não pode somar uma string com um inteiro sem conversão explícita. Os tipos primitivos são objetos com métodos e comportamentos ricos. None é o valor singleton que representa ausência — é falsy e tem tipo NoneType.

# int — precisão arbitrária (sem overflow)
x = 42
big = 10 ** 100     # googol — sem problema
hexval = 0xFF       # 255 em hexadecimal
binval = 0b1010     # 10 em binário
sep = 1_000_000     # underscore como separador visual

# float — ponto flutuante IEEE 754 de 64 bits
price = 29.90
ratio = 1.5e-3      # notação científica: 0.0015
import math
math.isfinite(price)  # True
math.isinf(float('inf'))  # True

# str — imutável, Unicode por padrão
name = "Rafael"
multiline = """
    primeira linha
    segunda linha
"""
raw = r"C:\Users\rafael\docs"  # raw string — \ não é escape

# bool — subclasse de int; True == 1, False == 0
active = True
print(int(active))   # 1
print(True + True)   # 2

# bytes — sequência de inteiros 0-255
data = b"hello"
encoded = "café".encode("utf-8")   # str → bytes
decoded = encoded.decode("utf-8")  # bytes → str

# None — valor singleton de ausência
result = None
print(result is None)   # True — use "is None", nunca "== None"

# Funções built-in de conversão
int("42")          # 42
int("ff", 16)      # 255 — base hexadecimal
float("3.14")      # 3.14
str(42)            # "42"
bool(0)            # False
bool("")           # False — strings vazias são falsy
bool("0")          # True  — strings não-vazias são truthy
list("abc")        # ['a', 'b', 'c']
tuple([1, 2, 3])   # (1, 2, 3)

# Funções úteis
abs(-5)            # 5
round(3.14159, 2)  # 3.14
divmod(17, 5)      # (3, 2) — quociente e resto
pow(2, 10)         # 1024
pow(2, 10, 1000)   # 24 — módulo eficiente (2^10 mod 1000)

Strings: métodos essenciais, f-strings avançadas, multiline, raw

Strings em Python são imutáveis e suportam uma API rica de métodos. f-strings (Python 3.6+) interpolam expressões diretamente, e o operador = para debug (3.8+) é especialmente útil em desenvolvimento.

# Métodos essenciais
s = "  Olá, Mundo!  "
s.strip()           # "Olá, Mundo!" — remove espaços
s.lstrip()          # "Olá, Mundo!  "
s.rstrip()          # "  Olá, Mundo!"
s.lower()           # "  olá, mundo!  "
s.upper()           # "  OLÁ, MUNDO!  "
s.title()           # "  Olá, Mundo!  "
s.replace("Mundo", "Python")  # "  Olá, Python!  "

"rafael,marques,dev".split(",")    # ['rafael', 'marques', 'dev']
", ".join(["um", "dois", "três"])  # "um, dois, três"

"hello world".startswith("hello")  # True
"hello world".endswith("world")    # True
"hello world".find("world")        # 6 — índice (-1 se não encontrar)
"hello world".count("l")           # 3

# Formatação
"R$ {:.2f}".format(29.9)           # "R$ 29.90"
"{:>10}".format("ok")              # "        ok" — alinha à direita

# f-strings — interpolação moderna e eficiente
name = "Rafael"
total = 1234.5
status = "paid"

f"Olá, {name}!"                    # "Olá, Rafael!"
f"Total: R$ {total:.2f}"           # "Total: R$ 1234.50"
f"Status: {status.upper()}"        # "Status: PAID"
f"Dobro: {total * 2}"              # "Dobro: 2469.0"

# = para debug (Python 3.8+) — imprime nome e valor
x = 42
print(f"{x=}")           # x=42
print(f"{total=:.2f}")   # total=1234.50

# !r, !s, !a — conversão antes de formatar
obj = Order(id="1")
f"{obj!r}"   # repr(obj)  — para debug
f"{obj!s}"   # str(obj)   — legível
f"{obj!a}"   # ascii(obj) — escapa non-ASCII

# f-strings com expressões complexas (Python 3.12+)
items = [1, 2, 3]
f"soma: {sum(items)}"
f"{'sim' if total > 1000 else 'não'}"

# Multiline f-string
msg = (
    f"Pedido #{order.id}\n"
    f"Cliente: {order.customer}\n"
    f"Total: R$ {order.total:.2f}"
)

# String com padding e alinhamento
f"{'esquerda':<20}"   # alinha à esquerda, largura 20
f"{'centro':^20}"     # centraliza
f"{'direita':>20}"    # alinha à direita
f"{42:05d}"           # 00042 — zero-pad
f"{255:#x}"           # 0xff — hexadecimal com prefixo

Listas: criação, slicing, métodos, comprehension

Listas são o array dinâmico de Python — mutáveis, heterogêneas e com API completa. Comprehensions são a forma idiomática de construir listas transformadas.

# Criação
empty = []
nums = [1, 2, 3, 4, 5]
mixed = [1, "texto", True, None, 3.14]
from_range = list(range(10))       # [0, 1, ..., 9]

# Slicing — [início:fim:passo], fim é exclusivo
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
nums[2:5]       # [2, 3, 4]
nums[:3]        # [0, 1, 2]
nums[7:]        # [7, 8, 9]
nums[::2]       # [0, 2, 4, 6, 8] — de 2 em 2
nums[::-1]      # [9, 8, 7, ..., 0] — reverso
nums[-3:]       # [7, 8, 9] — últimos 3

# Métodos principais
orders = []
orders.append(Order(id="1"))           # adiciona no fim — O(1)
orders.insert(0, Order(id="0"))        # insere em posição — O(n)
orders.extend([Order(id="2"), Order(id="3")])  # adiciona múltiplos
orders.remove(orders[0])               # remove por valor (primeira ocorrência)
last = orders.pop()                    # remove e retorna o último — O(1)
first = orders.pop(0)                  # remove e retorna o primeiro — O(n)
orders.sort(key=lambda o: o.total, reverse=True)  # ordena in-place
sorted_orders = sorted(orders, key=lambda o: o.total)  # nova lista ordenada
orders.reverse()                       # inverte in-place
idx = orders.index(target)             # índice da primeira ocorrência
count = orders.count(target)           # quantas vezes aparece
orders.clear()                         # remove todos

# List comprehension — [expressão for item in iterável if condição]
paid = [o for o in orders if o.status == "paid"]
totals = [o.total for o in orders]
ids_upper = [o.id.upper() for o in orders if o.total > 50]

# Aninhada — achata lista de listas
all_items = [item for order in orders for item in order.items]

# Com condição ternária
labels = ["caro" if o.total > 200 else "barato" for o in orders]

# Compreensão aninhada — cria matriz
matrix = [[row * col for col in range(1, 4)] for row in range(1, 4)]
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

# Truques úteis
flat = sum([[1, 2], [3, 4], [5]], [])  # achata lista (use itertools.chain para grandes)
unique_ordered = list(dict.fromkeys([3, 1, 2, 1, 3]))  # remove duplicatas preservando ordem

Tuples, Sets, Frozensets — quando usar cada um

Cada tipo de coleção tem propósito distinto. Tuple para dados estruturados imutáveis, set para operações de conjunto eficientes, frozenset para conjunto imutável que pode ser chave de dicionário.

# Tuple — imutável, heterogêneo, com semântica posicional
point = (3.0, 4.0)
rgb = (255, 128, 0)
single = (42,)        # vírgula obrigatória para tupla de um elemento
empty = ()

# Desempacotamento
x, y = point
r, g, b = rgb
first, *rest = [1, 2, 3, 4, 5]    # first=1, rest=[2, 3, 4, 5]
*start, last = [1, 2, 3, 4, 5]    # start=[1, 2, 3, 4], last=5

# Troca de variáveis — pythônico
a, b = b, a

# namedtuple — tuple com campos nomeados
from collections import namedtuple
Address = namedtuple("Address", ["street", "city", "zip_code"])
addr = Address(street="Rua A", city="São Paulo", zip_code="01310-100")
print(addr.city)     # "São Paulo"
print(addr[1])       # "São Paulo" — índice ainda funciona

# Set — mutável, sem duplicatas, operações de conjunto eficientes
tags = {"python", "backend", "api"}
tags.add("docker")
tags.discard("naoexiste")  # remove se existir, sem erro
tags.remove("python")      # remove, KeyError se não existir
"backend" in tags          # True — O(1)

# Operações de conjunto
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b    # {1, 2, 3, 4, 5, 6}  — união
a & b    # {3, 4}               — interseção
a - b    # {1, 2}               — diferença
a ^ b    # {1, 2, 5, 6}        — diferença simétrica
a <= b   # False — a é subconjunto de b?

# frozenset — imutável, pode ser chave de dict ou elemento de set
fs = frozenset({1, 2, 3})
cache = {fs: "resultado"}  # frozenset como chave — válido

# Quando usar cada um:
# list  → sequência ordenada mutável; posição importa
# tuple → dados imutáveis; retorno de função; chave de dict; namedtuple para semântica
# set   → unicidade; operações de conjunto; membership check rápido
# frozenset → conjunto imutável; chave de dict; elemento de outro set

Dicionários: acesso seguro, comprehension, merge, defaultdict, Counter

Dicionários são o tipo de dados mais versátil de Python. A partir do Python 3.7, a ordem de inserção é garantida. O |= para merge foi adicionado no Python 3.9.

# Criação
empty = {}
config = {"host": "localhost", "port": 5432, "ssl": False}
from_keys = dict.fromkeys(["a", "b", "c"], 0)  # {"a": 0, "b": 0, "c": 0}

# Acesso seguro
host = config["host"]              # KeyError se não existir
port = config.get("port", 3306)   # valor default se não existir
ssl = config.get("ssl")           # None se não existir (default de get)

# Verificação de existência
if "host" in config:
    print(config["host"])

# Operações comuns
config["user"] = "admin"          # inserir/atualizar
del config["ssl"]                 # remover — KeyError se não existir
popped = config.pop("user", None) # remove e retorna, None se não existir

# Iteração
for key in config:                # itera sobre chaves
    print(key)
for key, value in config.items():  # chave e valor
    print(f"{key}: {value}")
for value in config.values():      # apenas valores
    print(value)

# Dict comprehension
prices = {"a": 10.0, "b": 20.0, "c": 5.0}
doubled = {k: v * 2 for k, v in prices.items()}
filtered = {k: v for k, v in prices.items() if v > 8}

# Merge — Python 3.9+
defaults = {"timeout": 30, "retries": 3, "verbose": False}
overrides = {"timeout": 60, "verbose": True}

merged = defaults | overrides                # novo dict — overrides vence
defaults |= overrides                        # merge in-place

# Python 3.8 e anterior:
merged_old = {**defaults, **overrides}       # ainda válido em qualquer versão

# defaultdict — valor default automático para chaves ausentes
from collections import defaultdict

# Agrupa pedidos por status sem verificar se chave existe
by_status = defaultdict(list)
for order in orders:
    by_status[order.status].append(order)   # nunca KeyError

# Counter — conta frequências
from collections import Counter
words = ["go", "python", "go", "rust", "python", "go"]
freq = Counter(words)
print(freq)                    # Counter({'go': 3, 'python': 2, 'rust': 1})
print(freq.most_common(2))    # [('go', 3), ('python', 2)]
freq.update(["go", "java"])   # adiciona mais contagens

# OrderedDict — dict com operações de ordenação (Python 3.7+ dict já é ordered)
from collections import OrderedDict
od = OrderedDict([("b", 2), ("a", 1)])
od.move_to_end("b")           # move chave para o fim
od.move_to_end("a", last=False)  # move para o início

Control flow: if/elif/else, match/case (Python 3.10+)

O match/case introduzido no Python 3.10 é mais poderoso que um switch tradicional — suporta desestruturação de objetos, listas, dicionários, guards e classes. Vai além de simples comparação de valores.

# if / elif / else
status = "confirmed"
if status == "pending":
    process()
elif status in ("confirmed", "shipped"):
    notify()
elif status == "cancelled":
    refund()
else:
    raise ValueError(f"Status desconhecido: {status}")

# Expressão condicional (ternário)
label = "ativo" if user.active else "inativo"

# Walrus operator := (Python 3.8+) — atribui e testa numa expressão
import re
if m := re.search(r"\d+", text):
    print(f"Número encontrado: {m.group()}")

# match / case — Python 3.10+

# Matching de valor simples
def http_status(status: int) -> str:
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500 | 502 | 503:    # múltiplos valores com |
            return "Server Error"
        case _:                   # wildcard — equivalente ao default
            return "Unknown"

# Matching de estrutura de dicionário
def handle_event(event: dict) -> None:
    match event:
        case {"type": "order_placed", "id": order_id}:
            process_order(order_id)

        case {"type": "payment", "amount": amount} if amount > 10_000:
            flag_for_review(event)   # guard: condição extra após if

        case {"type": str(event_type)}:
            log.warning(f"Evento desconhecido: {event_type}")

        case _:
            log.error(f"Formato inválido: {event}")

# Matching de dataclasses e objetos
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

def classify_point(p: Point) -> str:
    match p:
        case Point(x=0, y=0):
            return "origem"
        case Point(x=0, y=y):
            return f"eixo Y em y={y}"
        case Point(x=x, y=0):
            return f"eixo X em x={x}"
        case Point(x=x, y=y) if x == y:
            return f"diagonal em {x}"
        case Point():
            return "outro ponto"

# Matching de sequências
def process_command(args: list[str]) -> None:
    match args:
        case ["quit"]:
            quit()
        case ["go", direction]:
            move(direction)
        case ["pick", "up", item]:
            pick_up(item)
        case ["drop", *items]:      # *items captura zero ou mais
            for item in items:
                drop(item)
        case _:
            print(f"Comando desconhecido: {args}")

Funções: *args, **kwargs, keyword-only, positional-only, closures

Python oferece controle fino sobre a assinatura de funções. O / marca o fim dos parâmetros positional-only (Python 3.8+). O * marca o início dos keyword-only. Defaults mutáveis são uma armadilha clássica.

# *args — captura argumentos posicionais extras como tuple
def soma(*nums: float) -> float:
    return sum(nums)

soma(1, 2, 3)      # 6.0
soma(*[1, 2, 3])   # expande lista

# **kwargs — captura argumentos keyword extras como dict
def create_order(**kwargs) -> dict:
    return {"status": "pending", **kwargs}  # kwargs sobrescreve defaults

create_order(customer_id="c-1", total=99.90)

# Misturando tudo
def full_example(required, *args, kw_only, **kwargs):
    pass

# / — parâmetros positional-only (não podem ser passados como keyword)
# Python 3.8+ — util para APIs onde o nome do parâmetro não é parte da interface pública
def connect(host, port, /, *, ssl=False, timeout=30):
    #     positional-only ↑ ↑ keyword-only
    pass

connect("localhost", 5432)              # OK
connect("localhost", 5432, ssl=True)    # OK
connect(host="localhost", port=5432)    # TypeError — host e port são positional-only

# Keyword-only — após * ou *args, devem ser passados por nome
def send_email(to: str, *, subject: str, body: str, cc: list[str] | None = None):
    pass

send_email("user@ex.com", subject="Olá", body="Texto")  # OK
# send_email("user@ex.com", "Olá", "Texto")  # TypeError — subject e body são keyword-only

# Armadilha: default mutável — compartilhado entre todas as chamadas!
def bad_append(item, lst=[]):  # NUNCA FAÇA ISSO
    lst.append(item)
    return lst

bad_append(1)  # [1]
bad_append(2)  # [1, 2] — lst é a MESMA lista!

# Correto: use None como sentinela
def good_append(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

# Closures — captura variáveis do escopo externo
def make_multiplier(factor: float):
    def multiply(x: float) -> float:
        return x * factor   # factor é capturado do escopo de make_multiplier
    return multiply

double = make_multiplier(2.0)
triple = make_multiplier(3.0)
print(double(5.0))   # 10.0
print(triple(5.0))   # 15.0

# nonlocal — modifica variável do escopo externo
def make_counter(start: int = 0):
    count = start
    def increment(by: int = 1) -> int:
        nonlocal count
        count += by
        return count
    return increment

Type hints: básico, Optional, Union, TypeVar, Generic, Protocol, Literal, TypedDict

Type hints são verificados por ferramentas como mypy e pyright. São puramente declarativos em runtime — não forçam tipos. A sintaxe moderna usa X | Y em vez de Union[X, Y] (Python 3.10+) e type X = ... em vez de TypeAlias (Python 3.12+).

from typing import TypeVar, Generic, Protocol, Literal, TypedDict, ParamSpec
from collections.abc import Callable, Sequence

# Básico
def find_user(user_id: str) -> dict[str, str] | None:
    return db.get(user_id)

def process(items: list[int], factor: float = 1.0) -> list[float]:
    return [x * factor for x in items]

# Optional[X] é equivalente a X | None
# Python 3.10+: use X | None diretamente
def get_name(user_id: str) -> str | None: ...

# TypeVar — variável de tipo para funções genéricas
T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")

def first(items: list[T]) -> T | None:
    return items[0] if items else None

def get_or_default(d: dict[K, V], key: K, default: V) -> V:
    return d.get(key, default)

# Generic — classe paramétrica
class Repository(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def add(self, item: T) -> None:
        self._items.append(item)

    def find_all(self) -> list[T]:
        return list(self._items)

order_repo = Repository[Order]()
order_repo.add(Order(id="1"))

# Protocol — duck typing estrutural (sem herança explícita)
class Notifiable(Protocol):
    def notify(self, message: str) -> None: ...

def alert_all(users: list[Notifiable], msg: str) -> None:
    for u in users:
        u.notify(msg)

# Qualquer objeto com método notify(str) satisfaz Notifiable
class EmailUser:
    def notify(self, message: str) -> None:
        send_email(self.email, message)

# Literal — restringe a valores específicos
Status = Literal["pending", "confirmed", "shipped", "delivered", "cancelled"]
HttpMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]

def update_status(order_id: str, status: Status) -> None: ...

# TypedDict — dict com campos tipados
class OrderDict(TypedDict):
    id: str
    customer_id: str
    total: float
    status: Status

class OrderDictPartial(TypedDict, total=False):
    notes: str       # campo opcional

# type alias (Python 3.12+)
type OrderId = str
type PriceMap = dict[str, float]
type Callback[T] = Callable[[T], None]  # alias genérico

# ParamSpec — preserva assinatura de funções em decorators
P = ParamSpec("P")
R = TypeVar("R")

def logged(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"chamando {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Dataclasses: @dataclass, field(), frozen, post_init, slots, KW_ONLY

@dataclass gera automaticamente __init__, __repr__ e __eq__. É a forma idiomática de criar classes de dados em Python moderno. frozen=True cria objetos imutáveis. KW_ONLY (Python 3.10+) torna campos keyword-only.

from dataclasses import dataclass, field, KW_ONLY
from datetime import datetime
from typing import ClassVar

@dataclass
class Order:
    id: str
    customer_id: str
    total: float
    status: str = "pending"                            # valor default simples
    created_at: datetime = field(default_factory=datetime.now)  # fábrica — correto
    items: list[str] = field(default_factory=list)    # NUNCA use [] como default
    _cache: dict = field(default_factory=dict, repr=False, compare=False)  # excluído do repr/eq

    # ClassVar — não é campo do dataclass
    _registry: ClassVar[dict[str, "Order"]] = {}

    def __post_init__(self) -> None:
        """Executado após __init__ gerado — validação e transformações."""
        if self.total < 0:
            raise ValueError(f"Total não pode ser negativo: {self.total}")
        self.id = self.id.strip()  # normalização
        Order._registry[self.id] = self

    def confirm(self) -> None:
        if self.status != "pending":
            raise ValueError(f"Não pode confirmar: status '{self.status}'")
        self.status = "confirmed"

# frozen=True — imutável, hashable, pode ser chave de dict ou elemento de set
@dataclass(frozen=True)
class Money:
    amount: int       # centavos — evita imprecisão de float
    currency: str = "BRL"

    def __add__(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("Moedas diferentes")
        return Money(self.amount + other.amount, self.currency)

m1 = Money(100, "BRL")
m2 = Money(200, "BRL")
total = m1 + m2  # Money(amount=300, currency='BRL')
# m1.amount = 500  # FrozenInstanceError — imutável

# __slots__ — elimina __dict__, reduz uso de memória (Python 3.10+: slots=True)
@dataclass(slots=True)
class Point:
    x: float
    y: float

# KW_ONLY — campos após ele são keyword-only (Python 3.10+)
@dataclass
class Config:
    host: str
    port: int
    _: KW_ONLY
    timeout: int = 30        # keyword-only
    ssl: bool = False        # keyword-only

cfg = Config("localhost", 5432, timeout=60, ssl=True)
# Config("localhost", 5432, 60, True) — TypeError — timeout e ssl são kw-only

# order=True — habilita comparação (<, >, <=, >=)
@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

v1 = Version(1, 2, 3)
v2 = Version(1, 3, 0)
print(v1 < v2)   # True

Classes: init, repr, eq, hash, slots, @property, @classmethod, @staticmethod

Classes em Python são templates flexíveis. Os dunder methods (double underscore) definem o comportamento com operadores e built-ins. @property cria atributos computados. @classmethod e @staticmethod alteram como o método recebe o primeiro argumento.

class BankAccount:
    """Conta bancária com validações."""

    _count: int = 0  # variável de classe — compartilhada por todas as instâncias

    def __init__(self, owner: str, balance: float = 0.0) -> None:
        self._owner = owner          # convenção: _ indica "protegido"
        self._balance = balance
        self.__secret = "privado"    # name mangling: _BankAccount__secret
        BankAccount._count += 1

    def __repr__(self) -> str:
        """Representação para debug — deve ser não-ambígua."""
        return f"BankAccount(owner={self._owner!r}, balance={self._balance:.2f})"

    def __str__(self) -> str:
        """Representação legível para o usuário final."""
        return f"Conta de {self._owner}: R$ {self._balance:.2f}"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, BankAccount):
            return NotImplemented
        return self._owner == other._owner and self._balance == other._balance

    def __hash__(self) -> int:
        """Necessário se __eq__ for definido e o objeto precisar ser hashable."""
        return hash((self._owner, self._balance))

    def __len__(self) -> int:
        """Permite len(account) — retorna algo com semântica."""
        return int(self._balance)

    # @property — atributo computado com getter
    @property
    def balance(self) -> float:
        return self._balance

    # setter — habilita account.balance = valor com validação
    @balance.setter
    def balance(self, value: float) -> None:
        if value < 0:
            raise ValueError("Saldo não pode ser negativo")
        self._balance = value

    # @classmethod — recebe a classe como primeiro argumento, não a instância
    # Use para factory methods e construtores alternativos
    @classmethod
    def from_dict(cls, data: dict) -> "BankAccount":
        return cls(owner=data["owner"], balance=data.get("balance", 0.0))

    @classmethod
    def total_accounts(cls) -> int:
        return cls._count

    # @staticmethod — não recebe nem classe nem instância
    # Use para funções utilitárias relacionadas à classe mas sem dependência de estado
    @staticmethod
    def validate_owner(name: str) -> bool:
        return bool(name and len(name) >= 2)

# Uso
acc = BankAccount("Rafael", 1000.0)
acc.balance = 1500.0       # usa o setter
print(acc.balance)          # usa o getter
print(repr(acc))            # BankAccount(owner='Rafael', balance=1500.00)
print(str(acc))             # Conta de Rafael: R$ 1500.00

Herança: super(), MRO, múltipla herança, ABC

Python suporta herança múltipla com a linearização C3 (MRO). super() delega para a próxima classe na MRO. Classes abstratas (ABC) definem contratos que subclasses devem implementar.

from abc import ABC, abstractmethod

# Classe base abstrata — define a interface
class PaymentGateway(ABC):

    @abstractmethod
    def charge(self, amount: float, customer_id: str) -> str:
        """Cobra o cliente e retorna o ID da transação."""
        ...

    @abstractmethod
    def refund(self, transaction_id: str) -> None:
        ...

    # Método concreto na classe abstrata — comportamento compartilhado
    def charge_and_log(self, amount: float, customer_id: str) -> str:
        txn_id = self.charge(amount, customer_id)
        print(f"Cobrança {txn_id}: R$ {amount:.2f} de {customer_id}")
        return txn_id

# Implementação concreta
class StripeGateway(PaymentGateway):
    def __init__(self, api_key: str) -> None:
        self.api_key = api_key

    def charge(self, amount: float, customer_id: str) -> str:
        # Chama Stripe API...
        return f"ch_stripe_{customer_id}"

    def refund(self, transaction_id: str) -> None:
        print(f"Stripe: estornando {transaction_id}")

# Herança com super()
class LoggedGateway(StripeGateway):
    def charge(self, amount: float, customer_id: str) -> str:
        print(f"[LOG] Iniciando cobrança de R$ {amount:.2f}")
        result = super().charge(amount, customer_id)  # delega para StripeGateway.charge
        print(f"[LOG] Cobrança concluída: {result}")
        return result

# MRO — Method Resolution Order
class A:
    def method(self): return "A"

class B(A):
    def method(self): return f"B -> {super().method()}"

class C(A):
    def method(self): return f"C -> {super().method()}"

class D(B, C):  # multiple inheritance
    pass

print(D().method())     # B -> C -> A
print(D.__mro__)        # [D, B, C, A, object]

# @abstractmethod com property
class Shape(ABC):
    @property
    @abstractmethod
    def area(self) -> float: ...

    @property
    @abstractmethod
    def perimeter(self) -> float: ...

class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    @property
    def area(self) -> float:
        return math.pi * self.radius ** 2

    @property
    def perimeter(self) -> float:
        return 2 * math.pi * self.radius

Decorators: functools.wraps, stacking, parametrized, class decorators

Um decorator é uma função que recebe outra função e retorna uma versão modificada. @functools.wraps preserva o nome, docstring e assinatura da função original — essencial para ferramentas de debug e documentação.

import functools, time, logging

# Decorator simples
def timed(func):
    @functools.wraps(func)  # preserva __name__, __doc__, __annotations__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - start
            logging.debug(f"{func.__name__} concluído em {elapsed:.3f}s")
            return result
        except Exception as e:
            elapsed = time.perf_counter() - start
            logging.error(f"{func.__name__} falhou após {elapsed:.3f}s: {e}")
            raise
    return wrapper

# Decorator parametrizado — fábrica que retorna o decorator
def retry(max_attempts: int = 3, delay: float = 0.5, exceptions: tuple = (Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_attempts:
                        logging.warning(f"{func.__name__} tentativa {attempt} falhou: {e}")
                        time.sleep(delay * attempt)  # backoff linear
            raise last_exception
        return wrapper
    return decorator

# Stacking — aplicados de baixo para cima
@retry(max_attempts=3, delay=1.0)
@timed
def fetch_orders(customer_id: str) -> list[dict]:
    return requests.get(f"/orders?customer={customer_id}").json()
# equivalente a: fetch_orders = retry(...)(timed(fetch_orders))

# Class decorator — state persistente entre chamadas
class RateLimit:
    """Limita chamadas por janela de tempo."""
    def __init__(self, max_calls: int, period: float):
        self.max_calls = max_calls
        self.period = period
        self.calls: list[float] = []

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.monotonic()
            self.calls = [t for t in self.calls if now - t < self.period]
            if len(self.calls) >= self.max_calls:
                raise RuntimeError(f"Rate limit: {self.max_calls} chamadas por {self.period}s")
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimit(max_calls=10, period=60.0)
def call_external_api(endpoint: str) -> dict: ...

Context managers: enter/exit, @contextmanager, ExitStack

O protocolo de context manager (with) garante cleanup mesmo com exceções. A implementação manual usa __enter__ e __exit__. O decorator @contextmanager simplifica com yield.

from contextlib import contextmanager, ExitStack, suppress

# Implementação manual — protocolo __enter__ / __exit__
class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self  # valor atribuído a "as"

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        return False  # False = não suprime exceções; True = suprime

with Timer() as t:
    heavy_computation()
print(f"Duração: {t.elapsed:.3f}s")

# @contextmanager — mais simples para maioria dos casos
@contextmanager
def db_transaction(session):
    """Gerencia transação de banco com commit/rollback automático."""
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()  # sempre executa

with db_transaction(get_session()) as session:
    session.add(Order(customer_id="c-1", total=99.90))

# @contextmanager como gerador de recursos
@contextmanager
def temp_directory():
    """Cria diretório temporário e remove ao sair."""
    import tempfile, shutil
    path = tempfile.mkdtemp()
    try:
        yield Path(path)
    finally:
        shutil.rmtree(path)

with temp_directory() as tmpdir:
    (tmpdir / "output.txt").write_text("dados")

# ExitStack — número dinâmico de context managers
def process_multiple_files(paths: list[Path]) -> None:
    with ExitStack() as stack:
        files = [stack.enter_context(open(p)) for p in paths]
        for f in files:
            process(f.read())

# suppress — suprime exceções específicas
with suppress(FileNotFoundError):
    os.remove("arquivo_que_pode_nao_existir.tmp")

# Parenthesized context managers (Python 3.10+)
with (
    open("input.txt") as infile,
    open("output.txt", "w") as outfile,
):
    outfile.write(infile.read().upper())

Iterators e Generators: iter/next, yield, yield from, send()

Generators são a forma idiomática de criar iteradores preguiçosos em Python. Não carregam todos os dados na memória — geram um item de cada vez. yield from delega para outro iterável.

# Protocolo de iterator — __iter__ e __next__
class CountDown:
    def __init__(self, start: int):
        self.current = start

    def __iter__(self):
        return self  # o próprio objeto é o iterator

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

for n in CountDown(5):
    print(n)  # 5, 4, 3, 2, 1

# Generator function — yield pausa e retoma a execução
def fibonacci(limit: int):
    a, b = 0, 1
    while a < limit:
        yield a
        a, b = b, a + b

for n in fibonacci(100):
    print(n)  # 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89

# Generator de arquivo grande — processa linha por linha sem carregar tudo
def read_orders(filepath: str):
    with open(filepath) as f:
        for line in f:
            line = line.strip()
            if line:
                yield parse_order(line)

for order in read_orders("orders.csv"):
    process(order)  # apenas uma linha em memória por vez

# yield from — delega para outro iterável
def chain_orders(*sources):
    for source in sources:
        yield from source  # equivale a: for item in source: yield item

# send() — comunicação bidirecional com generator
def accumulator():
    total = 0
    while True:
        value = yield total  # yield retorna total e recebe o próximo value
        if value is None:
            break
        total += value

acc = accumulator()
next(acc)              # avança até o primeiro yield — necessário para inicializar
acc.send(10)           # total = 10
acc.send(20)           # total = 30
acc.send(5)            # total = 35
acc.close()

# Generator expression — lazy, não cria lista em memória
total = sum(o.total for o in orders if o.status == "paid")
first_paid = next(o for o in orders if o.status == "paid")

Comprehensions: list, dict, set, generator — comparação de performance

Comprehensions são mais rápidas que loops for com append porque a operação de adição fica em C internamente. Generators são mais econômicos em memória para grandes volumes.

orders = get_orders()  # lista com N pedidos

# List comprehension — cria lista inteira em memória
paid_totals = [o.total for o in orders if o.status == "paid"]

# Dict comprehension
totals_by_id = {o.id: o.total for o in orders}

# Set comprehension — unicidade automática
unique_customers = {o.customer_id for o in orders}

# Generator expression — lazy, avaliado item por item
gen = (o.total for o in orders if o.status == "paid")
# gen ainda não calculou nada
grand_total = sum(gen)  # calcula apenas ao consumir

# Comparação de performance (aproximada para N grande):
#
# for + append:       100% tempo, 100% memória
# list comprehension:  60% tempo, 100% memória
# generator + sum:     65% tempo,   1% memória

# Quando usar cada um:
# list comp  → quando você precisa da lista inteira para iterar múltiplas vezes
# generator  → quando você processa uma vez (sum, any, all, next, for)
# dict comp  → quando precisa de lookup rápido por chave
# set comp   → quando precisa de unicidade ou operações de conjunto

# any/all com generator — curto-circuito: para na primeira correspondência
has_paid = any(o.status == "paid" for o in orders)
all_valid = all(o.total > 0 for o in orders)

# Comprehension aninhada — cuidado com legibilidade
# Prefira separar em variáveis ou funções se ficar complexo
matrix = [[i * j for j in range(1, 6)] for i in range(1, 6)]

# Comprehension com walrus (Python 3.8+) — reutiliza resultado da expressão
processed = [result for o in orders if (result := transform(o)) is not None]

Exceções: try/except/else/finally, Exception Groups (Python 3.11+)

O bloco else de um try executa apenas se não houver exceção — separa o caminho feliz do tratamento de erro. Exception Groups e except* (Python 3.11+) permitem tratar múltiplas exceções simultâneas de forma estruturada.

# Hierarquia de exceções — do específico para o geral
class DomainError(Exception):
    """Base para todas as exceções de domínio."""

class NotFoundError(DomainError):
    def __init__(self, resource: str, id: str) -> None:
        super().__init__(f"{resource} com id '{id}' não encontrado")
        self.resource = resource
        self.id = id

class ValidationError(DomainError):
    def __init__(self, field: str, message: str) -> None:
        super().__init__(f"Campo '{field}': {message}")
        self.field = field

# try / except / else / finally
def get_order(order_id: str) -> Order:
    try:
        order = db.find_order(order_id)         # pode lançar exceção
    except NotFoundError as e:
        log.warning(f"Pedido não encontrado: {e}")
        raise                                   # relança a mesma exceção
    except ConnectionError as e:
        raise DomainError(f"Banco indisponível") from e  # encadeia exceções
    else:
        log.info(f"Pedido {order_id} encontrado")  # executa apenas sem exceção
        return order
    finally:
        log.debug("get_order finalizado")       # executa SEMPRE

# raise from — encadeia exceções mantendo contexto
try:
    raw = json.loads(data)
except json.JSONDecodeError as e:
    raise ValidationError("body", "JSON inválido") from e

# Exception Groups e except* — Python 3.11+
# Útil quando múltiplas operações paralelas podem falhar simultaneamente

def validate_all(orders: list[Order]) -> None:
    errors = []
    for order in orders:
        try:
            validate(order)
        except ValidationError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("validação falhou", errors)

try:
    validate_all(orders)
except* ValidationError as eg:
    for exc in eg.exceptions:
        print(f"Validação: {exc}")
except* NotFoundError as eg:
    for exc in eg.exceptions:
        print(f"Não encontrado: {exc}")

# ExceptionGroup com asyncio — TaskGroup combina falhas em ExceptionGroup
import asyncio

async def fetch_all(ids: list[str]) -> list[Order]:
    results = []
    async with asyncio.TaskGroup() as tg:    # Python 3.11+
        tasks = {tg.create_task(fetch_order(id)): id for id in ids}
    # Se qualquer task falhar, ExceptionGroup é lançado com todas as falhas
    return [t.result() for t in tasks]

async/await: coroutines, asyncio, gather, TaskGroup, timeouts, aiohttp

async def define uma coroutine — uma função que pode pausar com await sem bloquear a thread. O event loop do asyncio multiplexa todas as coroutines em uma única thread. Use para I/O bound (HTTP, banco, arquivos); para CPU bound, use ProcessPoolExecutor.

import asyncio
import aiohttp

# Coroutine básica
async def fetch_order(session: aiohttp.ClientSession, order_id: str) -> dict:
    async with session.get(f"/api/orders/{order_id}") as resp:
        resp.raise_for_status()
        return await resp.json()

# asyncio.gather — executa múltiplas coroutines em paralelo
async def fetch_all_gather(order_ids: list[str]) -> list[dict]:
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_order(session, oid) for oid in order_ids]
        return await asyncio.gather(*tasks)  # falha se qualquer uma falhar

# asyncio.gather com return_exceptions — continua mesmo com falhas
async def fetch_all_safe(order_ids: list[str]) -> list[dict | Exception]:
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_order(session, oid) for oid in order_ids]
        return await asyncio.gather(*tasks, return_exceptions=True)

# TaskGroup (Python 3.11+) — substitui gather com melhor tratamento de erros
# Se qualquer task falhar, ExceptionGroup com todas as falhas é lançado
async def fetch_with_task_group(order_ids: list[str]) -> list[dict]:
    async with aiohttp.ClientSession() as session:
        async with asyncio.TaskGroup() as tg:
            tasks = [tg.create_task(fetch_order(session, oid)) for oid in order_ids]
    return [t.result() for t in tasks]

# Timeout — asyncio.timeout (Python 3.11+)
async def fetch_with_timeout(url: str) -> dict | None:
    try:
        async with asyncio.timeout(5.0):
            async with aiohttp.ClientSession() as s:
                async with s.get(url) as r:
                    return await r.json()
    except TimeoutError:
        return None

# asyncio.wait_for — alternativa para versões anteriores
async def fetch_old(url: str) -> dict | None:
    try:
        return await asyncio.wait_for(fetch(url), timeout=5.0)
    except asyncio.TimeoutError:
        return None

# create_task — inicia a task imediatamente (sem await)
async def background_processor():
    task = asyncio.create_task(long_running_job())
    # continua executando enquanto long_running_job roda em background
    await do_other_work()
    result = await task  # aguarda quando precisar do resultado

# asyncio.run — ponto de entrada para código síncrono
async def main():
    orders = await fetch_all_gather(["ord-1", "ord-2", "ord-3"])
    print(orders)

asyncio.run(main())  # cria event loop, executa e encerra

Functools: partial, lru_cache, cache, reduce, singledispatch

O módulo functools oferece utilidades de programação funcional. lru_cache é uma das otimizações mais simples e impactantes para funções computacionalmente caras.

import functools

# lru_cache — memoização com LRU cache (Least Recently Used)
@functools.lru_cache(maxsize=128)
def calculate_discount(customer_tier: str, total: float) -> float:
    """Resultados iguais são retornados do cache sem recalcular."""
    # chamada cara ao banco para buscar regras de desconto...
    return total * get_discount_rate(customer_tier)

# cache (Python 3.9+) — equivale a lru_cache(maxsize=None) — cache ilimitado
@functools.cache
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Inspecionar o cache
print(fibonacci.cache_info())   # CacheInfo(hits=..., misses=..., maxsize=None, currsize=...)
fibonacci.cache_clear()         # limpa o cache

# partial — "congela" argumentos de uma função
def send_notification(user_id: str, channel: str, message: str) -> None:
    ...

# Cria versão especializada com canal fixo
send_email = functools.partial(send_notification, channel="email")
send_sms   = functools.partial(send_notification, channel="sms")

send_email("user-1", message="Pedido confirmado")
send_sms("user-1", message="Pedido enviado")

# reduce — acumula resultado sobre um iterável
from functools import reduce
total = reduce(lambda acc, o: acc + o.total, orders, 0.0)

# singledispatch — polimorfismo baseado no tipo do primeiro argumento
from functools import singledispatch

@singledispatch
def serialize(obj) -> str:
    raise TypeError(f"Tipo não suportado: {type(obj)}")

@serialize.register(Order)
def _(obj: Order) -> str:
    return json.dumps({"id": obj.id, "total": obj.total})

@serialize.register(Customer)
def _(obj: Customer) -> str:
    return json.dumps({"id": obj.id, "name": obj.name})

@serialize.register(int)
@serialize.register(float)
def _(obj) -> str:
    return str(obj)

Pathlib e I/O: operações de Path, glob, leitura e escrita

pathlib.Path é a interface orientada a objetos para o sistema de arquivos. Substitui os.path com uma API muito mais ergonômica.

from pathlib import Path

# Criação de Path
base = Path("/home/rafael/projetos")
config = Path("config.yaml")           # relativo ao cwd
home = Path.home()                     # diretório home do usuário
cwd = Path.cwd()                       # diretório atual

# Navegação — operador / para concatenar
api = base / "minha-api"
src = api / "src" / "main.py"
print(src)                             # /home/rafael/projetos/minha-api/src/main.py

# Componentes do path
src.name        # "main.py"
src.stem        # "main"
src.suffix      # ".py"
src.parent      # /home/rafael/projetos/minha-api/src
src.parts       # ('/', 'home', 'rafael', 'projetos', 'minha-api', 'src', 'main.py')

# Verificações
src.exists()          # bool
src.is_file()         # bool
src.is_dir()          # bool
src.stat().st_size    # tamanho em bytes

# Leitura e escrita — API simples
text = src.read_text(encoding="utf-8")
src.write_text("novo conteúdo", encoding="utf-8")
data = src.read_bytes()
src.write_bytes(b"\x00\x01\x02")

# Criação de diretórios
output = base / "output"
output.mkdir(parents=True, exist_ok=True)  # cria todos os pais, sem erro se existir

# glob — encontra arquivos por padrão
py_files = list(base.glob("**/*.py"))         # recursivo
yaml_files = list(base.glob("*.yaml"))        # apenas no diretório atual
configs = list(base.rglob("config.yaml"))     # rglob = glob com **/ implícito

# iterdir — lista o conteúdo de um diretório
for item in base.iterdir():
    if item.is_dir():
        print(f"Dir: {item.name}")

# rename, copy, delete
old_path = base / "old_name.py"
new_path = base / "new_name.py"
old_path.rename(new_path)

import shutil
shutil.copy2(src, dst)          # copia com metadados
shutil.rmtree(output)           # remove diretório recursivamente
new_path.unlink(missing_ok=True)  # remove arquivo, sem erro se não existir

# Uso com open()
with (base / "data.json").open() as f:
    data = json.load(f)

Módulos avançados: itertools, collections, operator

itertools oferece funções de composição de iteradores de alto desempenho implementadas em C. collections tem estruturas de dados especializadas. operator tem versões funcionais dos operadores Python.

import itertools
from collections import deque, ChainMap
import operator

# itertools.chain — concatena iteráveis sem criar lista intermediária
all_items = list(itertools.chain(list1, list2, list3))
all_items_flat = list(itertools.chain.from_iterable([[1,2], [3,4], [5]]))

# itertools.groupby — agrupa elementos consecutivos iguais
# IMPORTANTE: os dados devem estar ORDENADOS pela chave antes de groupby
orders_sorted = sorted(orders, key=lambda o: o.status)
for status, group in itertools.groupby(orders_sorted, key=lambda o: o.status):
    group_list = list(group)
    print(f"{status}: {len(group_list)} pedidos")

# itertools.islice — fatia um iterável sem materializar
first_10 = list(itertools.islice(fibonacci_gen(), 10))

# itertools.product — produto cartesiano
sizes = ["P", "M", "G"]
colors = ["azul", "vermelho"]
variants = list(itertools.product(sizes, colors))
# [('P', 'azul'), ('P', 'vermelho'), ('M', 'azul'), ...]

# itertools.combinations e permutations
from itertools import combinations, permutations
list(combinations([1, 2, 3], 2))   # [(1,2), (1,3), (2,3)]
list(permutations([1, 2, 3], 2))   # [(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)]

# deque — fila de dupla extremidade, O(1) em ambos os lados
from collections import deque
queue = deque(maxlen=100)    # buffer circular
queue.appendleft("first")
queue.append("last")
queue.popleft()              # remove do início — O(1)

# ChainMap — visão encadeada de múltiplos dicts
defaults = {"timeout": 30, "retries": 3}
env_config = {"timeout": 60}
cli_args = {}

config = ChainMap(cli_args, env_config, defaults)  # prioridade: cli > env > defaults
print(config["timeout"])  # 60 — env_config sobrescreve defaults

# operator — versões funcionais dos operadores
from operator import attrgetter, itemgetter, methodcaller

# Ordenação mais eficiente que lambda
orders.sort(key=attrgetter("total"))              # equivale a lambda o: o.total
data.sort(key=itemgetter("total"))                # para dicts
names = list(map(attrgetter("name"), customers))  # extrai atributo de todos

# methodcaller
uppers = list(map(methodcaller("upper"), strings))
confirmed = list(filter(methodcaller("is_confirmed"), orders))

Testing com pytest: fixtures, parametrize, mocking, conftest

pytest é o framework de testes mais usado em Python. Fixtures gerenciam setup/teardown com injeção de dependência. @pytest.mark.parametrize é o equivalente de table-driven tests.

# test_orders.py
import pytest
from unittest.mock import MagicMock, patch, AsyncMock

# Fixture — setup reutilizável, injetado por nome no parâmetro
@pytest.fixture
def order() -> Order:
    return Order(id="test-1", customer_id="cust-1", total=99.90)

@pytest.fixture
def paid_order(order: Order) -> Order:
    order.status = "paid"
    return order

# Fixture de escopo mais amplo — criada uma vez por módulo/sessão
@pytest.fixture(scope="module")
def db_connection():
    conn = create_test_db()
    yield conn
    conn.close()  # cleanup após todos os testes do módulo

# @pytest.mark.parametrize — table-driven tests
@pytest.mark.parametrize("total, discount, expected", [
    (100.0, 0.1,  90.0),
    (200.0, 0.2, 160.0),
    (50.0,  0.0,  50.0),
    (0.0,   0.5,   0.0),
])
def test_apply_discount(total: float, discount: float, expected: float) -> None:
    order = Order(id="x", customer_id="c", total=total)
    apply_discount(order, discount)
    assert order.total == pytest.approx(expected)

# pytest.raises — testa que exceção é lançada
def test_negative_discount_raises(order: Order) -> None:
    with pytest.raises(ValueError, match="desconto não pode ser negativo"):
        apply_discount(order, -0.1)

# Mocking com pytest-mock (fixture mocker)
def test_send_notification(mocker) -> None:
    mock_send = mocker.patch("myapp.notifications.send_email")
    process_order(Order(id="1", customer_id="c-1", total=50.0))
    mock_send.assert_called_once_with("c-1", subject="Pedido confirmado")

# Mock de método de instância
def test_payment_service(mocker) -> None:
    mock_gateway = MagicMock(spec=PaymentGateway)
    mock_gateway.charge.return_value = "txn-123"
    service = OrderService(payment=mock_gateway)
    result = service.checkout(order)
    assert result.transaction_id == "txn-123"

# Teste assíncrono
@pytest.mark.asyncio
async def test_fetch_order_async(mocker) -> None:
    mock_response = AsyncMock(return_value={"id": "1", "status": "paid"})
    mocker.patch("myapp.client.get", mock_response)
    order = await fetch_order("1")
    assert order["status"] == "paid"

conftest.py — fixtures compartilhadas entre múltiplos arquivos de teste:

# conftest.py — automaticamente carregado pelo pytest
import pytest

@pytest.fixture(autouse=True)  # injetado em todos os testes sem precisar declarar
def reset_db():
    setup_test_database()
    yield
    teardown_test_database()

Packaging: pyproject.toml, virtual envs, uv

O padrão moderno de packaging Python usa pyproject.toml (PEP 517/518) em vez de setup.py. uv é o gerenciador de pacotes ultrarrápido (escrito em Rust) que substituiu pip + virtualenv + pip-tools em uma única ferramenta.

# pyproject.toml — arquivo padrão moderno de configuração de projeto
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "minha-api"
version = "1.0.0"
description = "API de pedidos"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.110.0",
    "sqlalchemy>=2.0.0",
    "pydantic>=2.0.0",
    "httpx>=0.27.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.0",
    "pytest-mock>=3.12.0",
    "mypy>=1.8.0",
    "ruff>=0.3.0",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

[tool.mypy]
strict = true
python_version = "3.11"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]  # pycodestyle, pyflakes, isort, pyupgrade
# uv — gerenciador ultrarrápido (substitui pip + venv + pip-tools)
uv init minha-api          # cria novo projeto com pyproject.toml
uv add fastapi             # adiciona dependência
uv add --dev pytest        # adiciona dependência de dev
uv remove requests         # remove dependência
uv sync                    # instala todas as deps do pyproject.toml
uv run python main.py      # executa no ambiente virtual do projeto
uv run pytest              # executa testes no ambiente correto

# Virtual env com uv
uv venv                    # cria .venv no diretório atual
source .venv/bin/activate  # ativa (Linux/macOS)
.venv\Scripts\activate     # ativa (Windows)

# uv pip — substituto drop-in para pip
uv pip install requests
uv pip freeze > requirements.txt
uv pip install -r requirements.txt

Versões — O que mudou

VersãoAnoPrincipais novidades
3.82019Walrus operator (:= — atribui dentro de expressão); f-string debug (f"{x=}"); parâmetros positional-only (/ na assinatura); typing.Protocol
3.92020Merge de dicts com | e |=; type hints sem import — list[int], dict[str, int], tuple[int, ...] em vez de List[int], Dict[str, int]
3.102021Structural pattern matching (match/case) com desestruturação de objetos, sequências e dicionários; X | Y para union types em type hints; dataclasses.KW_ONLY e slots=True; parenthesized context managers
3.11202210–60% mais rápido que 3.10 (especialização adaptativa); Exception Groups (ExceptionGroup + except*); asyncio.TaskGroup; asyncio.timeout(); tomllib na stdlib; tracebacks com seta apontando o erro exato
3.122023f-strings sem restrições (expressões complexas, aspas aninhadas); type X = ... para type aliases (PEP 695); @override decorator; pathlib.Path.walk(); @dataclass com melhorias de performance
3.132024JIT compiler experimental (--enable-experimental-jit); free-threaded mode sem GIL experimental (--disable-gil); REPL colorido e melhorado com autocompletion; melhorias em mensagens de erro; locals() retorna cópia independente

Async/Await Avançado: Queue, Semaphore, TaskGroup e async generators

O módulo asyncio é o coração da programação assíncrona em Python. Além do básico async def + await, há primitivas de concorrência avançadas que permitem orquestrar múltiplas corrotinas com controle fino de cancelamento, ordenação e back-pressure.

import asyncio
from asyncio import TaskGroup

# asyncio.gather() — executa corrotinas em paralelo e aguarda todas
async def buscar_dados(id: int) -> dict:
    await asyncio.sleep(0.1)
    return {"id": id, "valor": id * 10}

async def main_gather():
    resultados = await asyncio.gather(
        buscar_dados(1),
        buscar_dados(2),
        buscar_dados(3),
        return_exceptions=True   # erros viram valores, não cancelam as demais
    )
    for r in resultados:
        if isinstance(r, Exception):
            print(f"Falhou: {r}")
        else:
            print(r)

# asyncio.create_task() — agenda corrotina para rodar "em background"
async def main_tasks():
    task1 = asyncio.create_task(buscar_dados(1), name="busca-1")
    task2 = asyncio.create_task(buscar_dados(2), name="busca-2")
    # faz outro trabalho enquanto as tasks rodam...
    await asyncio.sleep(0)
    r1 = await task1
    r2 = await task2
    print(r1, r2)

# asyncio.wait() com FIRST_COMPLETED — reage à primeira que terminar
async def main_wait():
    tasks = {asyncio.create_task(buscar_dados(i)) for i in range(5)}
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
    for t in done:
        print("Primeiro resultado:", t.result())
    for t in pending:
        t.cancel()  # cancela as demais

# TaskGroup (Python 3.11+) — substitui gather() com cancelamento automático
async def main_taskgroup():
    async with TaskGroup() as tg:
        t1 = tg.create_task(buscar_dados(1))
        t2 = tg.create_task(buscar_dados(2))
    # ao sair do bloco, todas as tasks terminaram (ou a exceção foi propagada)
    print(t1.result(), t2.result())

# asyncio.Queue — padrão produtor/consumidor
async def produtor(queue: asyncio.Queue):
    for i in range(10):
        await queue.put(i)
        await asyncio.sleep(0.05)
    await queue.put(None)   # sentinel para sinalizar fim

async def consumidor(queue: asyncio.Queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(f"Processando: {item}")
        queue.task_done()

async def main_queue():
    q: asyncio.Queue[int | None] = asyncio.Queue(maxsize=5)
    async with TaskGroup() as tg:
        tg.create_task(produtor(q))
        tg.create_task(consumidor(q))

# asyncio.Semaphore — limita concorrência (ex: máx 3 requisições simultâneas)
async def fetch(session, url: str, sem: asyncio.Semaphore) -> str:
    async with sem:
        await asyncio.sleep(0.1)  # simula I/O
        return f"resposta de {url}"

async def main_semaphore():
    sem = asyncio.Semaphore(3)
    urls = [f"https://api.exemplo.com/{i}" for i in range(10)]
    tasks = [fetch(None, url, sem) for url in urls]
    resultados = await asyncio.gather(*tasks)
    print(resultados)

# async for — itera sobre async generators
async def gerador_numeros():
    for i in range(5):
        await asyncio.sleep(0.01)
        yield i

async def main_async_for():
    async for numero in gerador_numeros():
        print(numero)

# async with — gerenciadores de contexto assíncronos
class ConexaoDB:
    async def __aenter__(self):
        await asyncio.sleep(0.01)   # simula conexão
        return self

    async def __aexit__(self, *args):
        await asyncio.sleep(0.01)   # simula desconexão

async def main_async_with():
    async with ConexaoDB() as conn:
        print("Usando conexão:", conn)

Type Hints Modernos (Python 3.10+)

O sistema de tipos do Python evoluiu muito — a sintaxe ficou mais limpa e expressiva a partir da versão 3.10, com unions inline, pattern matching e novos tipos especiais do typing.

from __future__ import annotations   # habilita avaliação lazy de annotations em 3.9
from typing import TypeAlias, TypeVar, ParamSpec, TypeGuard, Self
from dataclasses import dataclass, field, KW_ONLY

# X | Y — union sem Union[] (Python 3.10+)
def processar(valor: int | str | None) -> str | None:
    if valor is None:
        return None
    return str(valor)

# match/case com type narrowing
def descrever(obj: int | str | list) -> str:
    match obj:
        case int(n) if n > 0:
            return f"inteiro positivo: {n}"
        case str(s) if s.startswith("http"):
            return f"URL: {s}"
        case list() as lst if len(lst) == 0:
            return "lista vazia"
        case [primeiro, *resto]:
            return f"lista com {len(resto)+1} itens, começa com {primeiro}"
        case _:
            return "outro"

# TypeAlias — nomeia tipos complexos (Python 3.10; `type` syntax no 3.12)
Matriz: TypeAlias = list[list[float]]
Callback: TypeAlias = Callable[[int, str], bool]

# No Python 3.12+ use a sintaxe nova:
# type Matriz = list[list[float]]

# TypeVar com bounds — T deve ser subclasse de Comparable
T = TypeVar("T", bound="Comparable")

def maximo(a: T, b: T) -> T:
    return a if a > b else b

# ParamSpec — captura a assinatura de uma função para decoradores tipados
from typing import Callable
P = ParamSpec("P")
R = TypeVar("R")

def logar(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"Chamando {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

# TypeGuard — narrowing customizado
def eh_lista_de_str(val: list) -> TypeGuard[list[str]]:
    return all(isinstance(item, str) for item in val)

def processar_lista(val: list[str | int]):
    if eh_lista_de_str(val):
        # aqui o type checker sabe que val é list[str]
        print(", ".join(val))

# Self — referência ao próprio tipo (útil em herança)
@dataclass
class Builder:
    _valor: int = 0

    def com_valor(self, v: int) -> Self:
        self._valor = v
        return self

    def build(self) -> int:
        return self._valor

# @dataclass com KW_ONLY — campos após KW_ONLY são keyword-only
@dataclass
class Config:
    nome: str
    _: KW_ONLY
    timeout: int = 30
    retries: int = 3
    debug: bool = False

cfg = Config("app", timeout=60, debug=True)   # timeout, retries, debug são keyword-only

Dataclasses vs Pydantic

@dataclass é a solução da biblioteca padrão para classes de dados estruturados. Pydantic adiciona validação de dados em tempo de execução com coerção de tipos. A escolha certa depende do caso de uso.

from dataclasses import dataclass, field
from typing import ClassVar

# @dataclass básico com field() e __post_init__
@dataclass
class Endereco:
    rua: str
    numero: int
    cidade: str
    cep: str = field(default="", repr=False)   # oculto no repr
    _contador: ClassVar[int] = 0               # variável de classe, não é campo

    def __post_init__(self):
        # validação/transformação após __init__
        if not self.cep:
            raise ValueError("CEP é obrigatório")
        self.cep = self.cep.replace("-", "")   # normaliza CEP

# frozen=True — imutável (hashable, pode ser usado em sets/dicts)
@dataclass(frozen=True)
class Coordenada:
    lat: float
    lon: float

c = Coordenada(-23.5, -46.6)
# c.lat = 0.0   # levanta FrozenInstanceError

# slots=True (Python 3.10+) — usa __slots__ → menos memória, mais rápido
@dataclass(slots=True)
class Ponto:
    x: float
    y: float
    z: float = 0.0

# Campos com factory (listas e dicts NUNCA como default direto)
@dataclass
class Pedido:
    id: int
    itens: list[str] = field(default_factory=list)
    metadados: dict[str, str] = field(default_factory=dict)
    criado_em: str = field(default_factory=lambda: "2024-01-01", init=False)
from pydantic import BaseModel, field_validator, model_validator, Field
from pydantic import EmailStr
from typing import Annotated

# Pydantic BaseModel — validação automática com coerção de tipos
class Usuario(BaseModel):
    id: int
    nome: str = Field(min_length=2, max_length=100)
    email: EmailStr
    idade: Annotated[int, Field(ge=0, le=150)]
    tags: list[str] = []

    @field_validator("nome")
    @classmethod
    def normalizar_nome(cls, v: str) -> str:
        return v.strip().title()

    @model_validator(mode="after")
    def validar_consistencia(self) -> "Usuario":
        if self.idade < 18 and "admin" in self.tags:
            raise ValueError("Menores não podem ser administradores")
        return self

# Criação com coerção automática de tipos
u = Usuario(id="42", nome="  rafael marques  ", email="r@r.com", idade="25")
print(u.nome)   # "Rafael Marques" — str coercida e normalizada
print(u.id)     # 42 — "42" (str) coercido para int

# Serialização / desserialização
dados = u.model_dump()                     # dict Python
dados_json = u.model_dump(mode="json")     # dict com tipos JSON-safe
json_str = u.model_dump_json()             # str JSON

# Desserializar
u2 = Usuario.model_validate({"id": 1, "nome": "Ana", "email": "a@a.com", "idade": 30})
u3 = Usuario.model_validate_json('{"id":2,"nome":"Bob","email":"b@b.com","idade":25}')

Quando usar cada um:

Critério@dataclassPydantic
Validação de dados de entrada (API, JSON, CSV)TrabalhosoNativo
Performance (objetos internos)Mais rápidoOverhead de validação
Coerção de tipos ("42"42)NãoSim
Serialização JSONManual / dataclasses.asdict()model_dump_json() nativo
Dependência externaNenhuma (stdlib)Pydantic (instalar)
Imutabilidade (frozen)Simmodel_config = ConfigDict(frozen=True)
Integração com FastAPI, SQLModelIndiretaNativa
Herança complexaSimSim

Regra prática: use @dataclass para objetos internos do domínio sem I/O externo; use Pydantic para DTOs que cruzam fronteiras (JSON de API, variáveis de ambiente com pydantic-settings, configs).


Context Managers Avançados: asynccontextmanager, suppress e ExitStack

Context managers garantem que recursos sejam adquiridos e liberados corretamente, mesmo em caso de exceção. A interface é __enter__ / __exit__; a sintaxe é with.

from contextlib import contextmanager, asynccontextmanager, suppress, ExitStack
import contextlib

# Implementação via dunder methods
class Transacao:
    def __init__(self, conexao):
        self.conexao = conexao

    def __enter__(self):
        self.conexao.begin()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            self.conexao.commit()
        else:
            self.conexao.rollback()
        return False  # False = não suprime a exceção; True = suprime

# @contextmanager — forma mais pythônica (usa generator)
@contextmanager
def abrir_arquivo_seguro(path: str, modo: str = "r"):
    arq = None
    try:
        arq = open(path, modo, encoding="utf-8")
        yield arq          # tudo antes do yield é __enter__
    except OSError as e:
        print(f"Erro ao abrir {path}: {e}")
        raise
    finally:
        if arq:
            arq.close()    # tudo no finally é __exit__

with abrir_arquivo_seguro("dados.txt") as f:
    conteudo = f.read()

# contextlib.suppress — suprime exceções específicas (elegante, sem try/except)
with suppress(FileNotFoundError, PermissionError):
    import os
    os.remove("arquivo_temporario.txt")

# equivalente verboso:
# try:
#     os.remove("arquivo_temporario.txt")
# except (FileNotFoundError, PermissionError):
#     pass

# @asynccontextmanager — versão assíncrona
import asyncio

@asynccontextmanager
async def conexao_db(dsn: str):
    conn = None
    try:
        conn = await asyncio.sleep(0)   # simula await de conexão
        yield conn
    finally:
        if conn is not None:
            pass   # simula fechar conexão

async def usar_db():
    async with conexao_db("postgresql://...") as conn:
        pass   # usa conn

# ExitStack — combina múltiplos context managers dinamicamente
def processar_arquivos(caminhos: list[str]):
    with ExitStack() as stack:
        arquivos = [stack.enter_context(open(p, encoding="utf-8")) for p in caminhos]
        # todos os arquivos são fechados automaticamente ao sair do bloco
        for arq in arquivos:
            print(arq.readline())

# ExitStack com rollback condicional
def operacao_critica():
    with ExitStack() as stack:
        stack.callback(print, "Limpeza executada")   # sempre executa ao sair
        # adiciona callbacks condicionalmente conforme a operação avança
        recurso = "adquirido"
        stack.callback(lambda: print(f"Liberando: {recurso}"))
        # se uma exceção ocorrer, todos os callbacks registrados são executados

Decoradores Avançados: factory, cache, class-based e retry com backoff

Decoradores são funções que recebem uma função (ou classe) e retornam outra função (ou classe). Dominá-los é essencial para escrever código DRY e expressivo em Python.

import functools
import time
from typing import Callable, TypeVar, ParamSpec

P = ParamSpec("P")
R = TypeVar("R")

# Decorador simples — sem argumentos
def logar(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)   # preserva __name__, __doc__, __annotations__
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print(f"→ {func.__name__}({args}, {kwargs})")
        resultado = func(*args, **kwargs)
        print(f"← {func.__name__} = {resultado}")
        return resultado
    return wrapper

# Decorador com argumentos — factory de decorador
def limitar_taxa(chamadas: int, periodo: float):
    """Limita a N chamadas por período de tempo."""
    def decorador(func: Callable[P, R]) -> Callable[P, R]:
        historico: list[float] = []

        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            agora = time.monotonic()
            historico[:] = [t for t in historico if agora - t < periodo]
            if len(historico) >= chamadas:
                raise RuntimeError(f"Limite de {chamadas} chamadas/{periodo}s excedido")
            historico.append(agora)
            return func(*args, **kwargs)
        return wrapper
    return decorador

@limitar_taxa(chamadas=5, periodo=1.0)
def buscar_api(endpoint: str) -> dict:
    return {"endpoint": endpoint}

# functools.cache — memoização sem limite (Python 3.9+)
@functools.cache
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# functools.lru_cache — memoização com limite de entradas
@functools.lru_cache(maxsize=128)
def calcular_fatorial(n: int) -> int:
    return 1 if n == 0 else n * calcular_fatorial(n - 1)

print(calcular_fatorial.cache_info())   # hits, misses, maxsize, currsize

# Class-based decorator — útil quando precisa de estado
class Tentativas:
    """Repete a chamada até N vezes em caso de exceção."""

    def __init__(self, max_tentativas: int = 3, excecoes: tuple = (Exception,)):
        self.max_tentativas = max_tentativas
        self.excecoes = excecoes

    def __call__(self, func: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            ultimo_erro = None
            for tentativa in range(1, self.max_tentativas + 1):
                try:
                    return func(*args, **kwargs)
                except self.excecoes as e:
                    ultimo_erro = e
                    print(f"Tentativa {tentativa}/{self.max_tentativas} falhou: {e}")
            raise ultimo_erro
        return wrapper

@Tentativas(max_tentativas=3, excecoes=(ConnectionError, TimeoutError))
def chamar_servico_externo(url: str) -> dict:
    pass   # implementação real aqui

# Decorador de retry com backoff exponencial
def retry(max_tentativas: int = 3, backoff: float = 1.0, excecoes: tuple = (Exception,)):
    def decorador(func: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            for tentativa in range(max_tentativas):
                try:
                    return func(*args, **kwargs)
                except excecoes as e:
                    if tentativa == max_tentativas - 1:
                        raise
                    espera = backoff * (2 ** tentativa)   # 1s, 2s, 4s, ...
                    print(f"Aguardando {espera:.1f}s antes da tentativa {tentativa + 2}...")
                    time.sleep(espera)
        return wrapper
    return decorador

@retry(max_tentativas=4, backoff=0.5, excecoes=(ConnectionError,))
def conectar_banco(host: str) -> object:
    pass   # tenta conectar; lança ConnectionError se falhar

# Empilhando decoradores — a ordem importa (de baixo para cima no wrap)
@logar
@retry(max_tentativas=3)
@limitar_taxa(chamadas=10, periodo=60.0)
def operacao_importante(id: int) -> str:
    return f"resultado-{id}"
# wrap order: limitar_taxa → retry → logar