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 prefixoListas: 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 ordemTuples, 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 setDicioná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ícioControl 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 incrementType 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 wrapperDataclasses: @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) # TrueClasses: 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.00Heranç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.radiusDecorators: 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 encerraFunctools: 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.txtVersões — O que mudou
| Versão | Ano | Principais novidades |
|---|---|---|
| 3.8 | 2019 | Walrus operator (:= — atribui dentro de expressão); f-string debug (f"{x=}"); parâmetros positional-only (/ na assinatura); typing.Protocol |
| 3.9 | 2020 | Merge de dicts com | e |=; type hints sem import — list[int], dict[str, int], tuple[int, ...] em vez de List[int], Dict[str, int] |
| 3.10 | 2021 | Structural 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.11 | 2022 | 10–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.12 | 2023 | f-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.13 | 2024 | JIT 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-onlyDataclasses 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 | @dataclass | Pydantic |
|---|---|---|
| Validação de dados de entrada (API, JSON, CSV) | Trabalhoso | Nativo |
| Performance (objetos internos) | Mais rápido | Overhead de validação |
Coerção de tipos ("42" → 42) | Não | Sim |
| Serialização JSON | Manual / dataclasses.asdict() | model_dump_json() nativo |
| Dependência externa | Nenhuma (stdlib) | Pydantic (instalar) |
Imutabilidade (frozen) | Sim | model_config = ConfigDict(frozen=True) |
| Integração com FastAPI, SQLModel | Indireta | Nativa |
| Herança complexa | Sim | Sim |
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 executadosDecoradores 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