Um guia abrangente de boas práticas em Python
Um guia de boas práticas para desenvolver em Python. Inspirado pelo gist de Rui Maranhao.
Em geral
“O belo é melhor que o feio.” - PEP 20
Diretrizes gerais de desenvolvimento
“Explícito é melhor que implícito” - PEP 20
Sim
def process_data(data, encoding='utf-8', timeout=30):
"""Processa dados com parâmetros explícitos."""
# Parâmetros são claros e visíveis
return data.decode(encoding)
Não
def process_data(data):
"""Processa dados com valores ocultos."""
# Valores mágicos escondidos na função
return data.decode('utf-8') # Qual encoding? Por quê?
“Legibilidade conta.” - PEP 20
Sim
def calculate_total_price(items, tax_rate):
subtotal = sum(item.price for item in items)
tax = subtotal * tax_rate
total = subtotal + tax
return total
Não
def calc(i, t):
return sum(x.p for x in i) * (1 + t)
“Qualquer pessoa pode corrigir qualquer coisa.”
Não crie barreiras artificiais ou “propriedade” do código. Se você encontrar um bug ou uma oportunidade de melhoria em qualquer parte do código, corrija.
Este princípio vem da filosofia de desenvolvimento da Khan Academy.
Sim
- Viu um erro de digitação no módulo de outra pessoa? Corrija.
- Encontrou um bug no código de outro time? Envie uma correção.
- Notou documentação desatualizada? Atualize.
Não
- “Esse não é meu módulo, não vou mexer.”
- “Vou contornar esse bug no código deles.”
- Deixar código quebrado para o “dono” corrigir.
Corrija cada problema (má decisão de design, decisão errada ou código ruim) assim que for descoberto.
Sim
# Você percebe durante a revisão de código que uma função faz demais.
# Refatore imediatamente:
def process_user_data(user):
validate_user(user)
save_to_database(user)
send_welcome_email(user)
Não
# Você percebe o problema mas adiciona um comentário TODO:
def process_user_data(user):
# TODO: Esta função faz demais, deveria ser refatorada
validate_user(user)
save_to_database(user)
send_welcome_email(user)
# TODO provavelmente nunca será resolvido
“Agora é melhor que nunca.” - PEP 20
Não espere pela solução “perfeita”. Implemente código funcional, depois itere.
Sim
- Implemente uma funcionalidade básica, publique, obtenha feedback, melhore.
- Escreva testes simples agora ao invés de esperar para desenhar a suíte de testes perfeita.
Não
- Debater infinitamente a arquitetura ideal sem escrever código.
- Esperar meses para publicar porque quer tratar todos os casos extremos.
Teste sem piedade. Escreva documentação para novas funcionalidades.
Sim
def divide(a, b):
"""Divide a por b.
:param a: Numerador
:param b: Denominador (deve ser diferente de zero)
:returns: Resultado de a / b
:raises ValueError: Se b for zero
"""
if b == 0:
raise ValueError("Não é possível dividir por zero")
return a / b
# E testes correspondentes:
class TestDivide(unittest.TestCase):
def test_divide_positive_numbers(self):
self.assertEqual(divide(10, 2), 5)
def test_divide_by_zero_raises_error(self):
with self.assertRaises(ValueError):
divide(10, 0)
Não
def divide(a, b):
return a / b # Sem docs, sem testes, vai quebrar com zero
Mais importante que Desenvolvimento Orientado a Testes – Desenvolvimento Orientado a Humanos
Escreva código para humanos primeiro, máquinas depois.
Sim
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
"""Adiciona um item ao carrinho."""
self.items.append(item)
def get_total(self):
"""Calcula o preço total de todos os itens."""
return sum(item.price for item in self.items)
Não
class SC:
def __init__(self):
self.i = []
def a(self, x):
self.i.append(x)
def t(self):
return sum(z.p for z in self.i)
Essas diretrizes podem – e provavelmente vão – mudar.
Seja flexível e aberto a melhorar práticas conforme aprende e conforme o campo evolui. O que funciona hoje pode não ser a melhor abordagem amanhã.
Em particular
Estilo
Siga o PEP 8, quando fizer sentido.
Nomenclatura
- Variáveis, funções, métodos, pacotes, módulos
lower_case_with_underscores
- Classes e Exceções
CapWords
- Métodos protegidos e funções internas
_single_leading_underscore(self, ...)- Nota: Começar com underscores ajudam IDEs a identificar membros protegidos/privados e podem causar avisos de “não usado” para helpers internos
- Métodos privados
__double_leading_underscore(self, ...)
- Constantes
ALL_CAPS_WITH_UNDERSCORES
Sobre prefixos com underscore:
Usar prefixo _ para funções protegidas/privadas ajuda IDEs e linters a entender sua intenção:
# mymodule.py
def _internal_helper(data):
"""Função interna - não faz parte da API pública."""
return data.strip().lower()
def public_function(text):
"""Função da API pública."""
return _internal_helper(text)
# IDE vai avisar que _internal_helper está "não usada" se não for chamada internamente
# IDE vai marcar como protegida/interna na navegação de código
Racional: IDEs como PyCharm e VS Code usam o prefixo underscore para:
- Ativar avisos de “código não usado” para helpers internos
- Indicar que essas funções não devem ser importadas com
from module import * - Mostrar ícones/cores diferentes na navegação para distinguir APIs públicas de internas
Diretrizes gerais de nomenclatura
Evite variáveis de uma letra (especialmente l, O, I).
Exceção: Em blocos muito curtos, quando o significado é claramente visível pelo contexto imediato
Ok
for e in elements:
e.mutate()
Evite nomes redundantes.
Sim
import audio
core = audio.Core()
controller = audio.Controller()
Não
import audio
from audio import *
core = audio.AudioCore()
controller = audio.AudioController()
Prefira “notação reversa”.
Sim
elements = ...
elements_active = ...
elements_defunct = ...
Não
elements = ...
active_elements = ...
defunct_elements ...
Evite reutilizar o mesmo nome de variável para propósitos diferentes.
Sim
# Cada variável tem um propósito único e claro
user_input = input("Digite um número: ")
user_number = int(user_input)
squared_number = user_number ** 2
print(f"Resultado: {squared_number}")
# Processando diferentes tipos de dados
raw_data = fetch_data_from_api()
processed_data = clean_data(raw_data)
validated_data = validate_data(processed_data)
Não
# 'data' significa algo diferente a cada vez
data = input("Digite um número: ") # data é string
data = int(data) # agora é int
data = data ** 2 # agora é o resultado ao quadrado
print(f"Resultado: {data}") # confuso!
# Reutilizando 'result' para coisas não relacionadas
result = calculate_tax(price)
save_to_database(result)
result = send_email(user) # agora result é outra coisa
result = validate_input(form) # e agora outra ainda
Racional: Reutilizar nomes de variáveis dificulta depuração, entendimento e manutenção. Cada variável deve representar um conceito durante todo seu escopo.
Indentação
Fica a seu critério, mas seja consistente. É isso.
No entanto, note: Um tab pode ter um número diferente de colunas dependendo do seu ambiente, mas um espaço é sempre uma coluna. O mais importante é ser consistente em todo o seu código, não usar um valor específico de tabulação.
Comparação de igualdade
Evite comparar com True, False ou None.
Sim
if attr:
print('True!')
if attr is True:
print('True!')
if not attr:
print('False!')
if attr is None:
print('None')
Não
if attr == True:
print('True!')
if attr == False:
print('False!')
if attr == None:
print('None')
List comprehensions
Use list comprehensions sempre que possível.
Sim
a = [3, 4, 5]
b = [i for i in a if i > 4]
# Ou (filter neste caso; map pode ser mais apropriado em outros casos)
b = filter(lambda x: x > 4, a)
Não
a = [3, 4, 5]
b = []
for i in a:
if i > 4:
b.append(i)
Palavra-chave with e arquivos
A instrução with garante que o código de limpeza seja executado. Ao abrir um arquivo, with garante que o arquivo será fechado após o bloco.
Sim
with open('file.txt') as f:
do_something_with_f
Não
f = open('file.txt')
do_something_with_f
f.close()
Imports
Importe módulos inteiros ao invés de símbolos individuais. Por exemplo, para um módulo de topo canteen que tem um arquivo canteen/sessions.py:
Sim
import canteen
import canteen.sessions
from canteen import sessions
Não
from canteen import get_user # Símbolo de canteen/__init__.py
from canteen.sessions import get_session # Símbolo de canteen/sessions.py
Exceção: Para código de terceiros onde a documentação diz explicitamente para importar símbolos individuais.
Racional: Evita imports circulares. Veja aqui.
Coloque todos os imports no topo do arquivo em três seções, cada uma separada por uma linha em branco, nesta ordem:
- Imports do sistema
- Imports de terceiros
- Imports do projeto local
Racional: Fica claro de onde cada módulo está vindo.
Documentação
Siga as diretrizes de docstring do PEP 257. reStructured Text e Sphinx podem ajudar a reforçar esses padrões.
Quando possível, use docstrings de uma linha para funções óbvias.
"""Retorna o caminho de ``foo``."""
Docstrings multilinha devem incluir:
- Linha de resumo
- Caso de uso, se apropriado
- Argumentos
- Tipo de retorno e semântica, a menos que retorne
None
"""Treina um modelo para classificar Foos e Bars.
Uso::
>>> import klassify
>>> data = [("green", "foo"), ("orange", "bar")]
>>> classifier = klassify.train(data)
:param train_data: Uma lista de tuplas no formato ``(color, label)``.
:rtype: Um :class:`Classifier <Classifier>`
"""
Notas
- Use verbos de ação (“Retorna”) ao invés de descrições (“Retorno”).
- Documente métodos
__init__na docstring da classe.
class Person(object):
"""Uma representação simples de um ser humano.
:param name: String, nome da pessoa.
:param age: Int, idade da pessoa.
"""
def __init__(self, name, age):
self.name = name
self.age = age
Formatos alternativos de docstring
Além do reStructuredText (reST), há outros estilos populares de docstring na comunidade Python. Escolha um e seja consistente no projeto.
Estilo NumPy/SciPy
Popular em computação científica, mais legível para funções complexas com muitos parâmetros.
def calculate_statistics(data, weights=None, ddof=0):
"""
Calcula média e desvio padrão dos dados.
Parâmetros
----------
data : array_like
Array de dados de entrada.
weights : array_like, opcional
Pesos para cada valor em data. Padrão é None.
ddof : int, opcional
Delta dos graus de liberdade. Padrão é 0.
Retorno
-------
mean : float
Média aritmética dos dados.
std : float
Desvio padrão dos dados.
Exceções
--------
ValueError
Se data estiver vazio.
Exemplos
--------
>>> calculate_statistics([1, 2, 3, 4, 5])
(3.0, 1.4142135623730951)
"""
pass
Estilo Google
Limpo e legível, popular em muitos projetos open-source.
def calculate_statistics(data, weights=None, ddof=0):
"""Calcula média e desvio padrão dos dados.
Args:
data (array_like): Array de dados de entrada.
weights (array_like, opcional): Pesos para cada valor em data. Padrão é None.
ddof (int, opcional): Delta dos graus de liberdade. Padrão é 0.
Retorna:
tuple: Uma tupla contendo:
- mean (float): Média aritmética dos dados.
- std (float): Desvio padrão dos dados.
Exceções:
ValueError: Se data estiver vazio.
Exemplos:
>>> calculate_statistics([1, 2, 3, 4, 5])
(3.0, 1.4142135623730951)
"""
pass
Comparação
| Estilo | Melhor para | Ferramentas |
|---|---|---|
| reStructuredText | Projetos Python gerais, documentação Sphinx | Sphinx, maioria das IDEs |
| NumPy/SciPy | Computação científica, projetos de dados | Sphinx com extensão Napoleon |
| Docs limpas, projetos estilo Google | Sphinx com extensão Napoleon |
Ferramentas de Geração de Documentação
Além do Sphinx, várias outras ferramentas podem gerar documentação a partir de suas docstrings:
| Ferramenta | Características | Melhor para |
|---|---|---|
| pdoc | Docs HTML minimalistas e limpas; gera automaticamente de docstrings; ótimo para pequenos projetos | Documentação rápida sem configuração |
| MkDocs com mkdocstrings | Tema Material Design moderno; baseado em Markdown; integra docstrings | Documentação bonita e moderna do projeto |
| Pydoc | Ferramenta Python nativa; mínima configuração; gera HTML ou docs de terminal | Projetos simples; documentação sem dependências |
| Quartodoc | Conecta Quarto e Python; suporta múltiplos formatos de docstring | Projetos de data science; integrando código com narrativas |
| ReadTheDocs | Hospedagem gratuita; integra com Sphinx; compilação automática do GitHub | Projetos open-source que precisam de documentação hospedada |
| Griffe | Parser de doc Python moderno; suporta múltiplos formatos; amigável a async | Projetos que requerem extração flexível de docstrings |
Pontos-chave
- Seja consistente: Escolha um estilo e use em todo o projeto
- Suporte de ferramentas: Maioria dos geradores suportam múltiplos formatos de docstring
- Preferência do time: Siga a convenção do projeto ou equipe
- Legibilidade: NumPy e Google são mais legíveis para funções complexas
- Integração: Considere quais ferramentas se integram com sua plataforma de CI/CD e hospedagem
Sobre comentários
Use com moderação. Prefira legibilidade do código a muitos comentários. Muitas vezes, métodos pequenos são mais eficazes que comentários.
Não
# Se o sinal for de parada
if sign.color == 'red' and sign.sides == 8:
stop()
Sim
def is_stop_sign(sign):
return sign.color == 'red' and sign.sides == 8
if is_stop_sign(sign):
stop()
Ao escrever comentários, lembre-se: “Strunk e White” se aplicam. - PEP 8
Resumindo:
Use linguagem clara e direta e evite palavras desnecessárias para garantir o entendimento do leitor, como recomendado por Strunk e White.
Comprimento de linhas
Não se preocupe demais. 80-100 caracteres está ótimo. Temos telas largas hoje em dia.
Use parênteses para continuar linhas.
wiki = (
"The Colt Python is a .357 Magnum caliber revolver formerly manufactured "
"by Colt's Manufacturing Company of Hartford, Connecticut. It is sometimes "
'referred to as a "Combat Magnum". It was first introduced in 1955, the '
"same year as Smith & Wesson's M29 .44 Magnum."
)
Formatação de strings
Use f-strings (literais formatados) para formatar strings. São mais legíveis, concisas e rápidas que métodos antigos.
Sim
name = "Alice"
age = 30
city = "Lisboa"
# Interpolação simples de variável
message = f"Olá, {name}!"
# Expressões dentro de f-strings
info = f"{name} tem {age} anos e mora em {city}."
# Formatando números
price = 19.99
quantity = 3
total = f"Total: €{price * quantity:.2f}" # Total: €59.97
# f-string multi-linha
report = (
f"Relatório do usuário:\n"
f" Nome: {name}\n"
f" Idade: {age}\n"
f" Cidade: {city}"
)
Não
name = "Alice"
age = 30
city = "Lisboa"
# Formatação estilo % (evite)
message = "Olá, %s!" % name
info = "%s tem %d anos e mora em %s." % (name, age, city)
# Método str.format() (verboso)
message = "Olá, {}!".format(name)
info = "{} tem {} anos e mora em {}.".format(name, age, city)
# Concatenação de strings (propenso a erros e difícil de ler)
message = "Olá, " + name + "!"
info = name + " tem " + str(age) + " anos e mora em " + city + "."
Racional: F-strings (Python 3.6+) são mais rápidas, legíveis e menos propensas a erro que % ou .format(). Permitem expressões diretamente na string e tornam a intenção do código mais clara.
Recursos avançados de f-string
# Debug com f-strings (Python 3.8+)
x = 10
y = 20
print(f"{x=}, {y=}, {x+y=}") # x=10, y=20, x+y=30
# Chamando funções
def get_status():
return "ativo"
status_msg = f"Sistema está {get_status()}"
Formatando números com f-strings
# Formatar float com 2 casas decimais
price = 19.98765
formatted_price = f"{price:.2f}" # '19.99'
# Inteiro com zeros à esquerda (ex: 4 dígitos)
order_number = 42
formatted_order = f"{order_number:04d}" # '0042'
# Alinhar uma string à direita com espaços usando f-strings
texto = "Python"
largura = 12
alinhado_direita = f"{texto:>{largura}}"
print(f"'{alinhado_direita}'") # Saída: ' Python'
# Ou, para alinhamento à esquerda (para comparação):
alinhado_esquerda = f"{texto:<{largura}}"
print(f"'{alinhado_esquerda}'") # Saída: 'Python '
# Porcentagem com 1 casa decimal
success_rate = 0.857
formatted_rate = f"{success_rate:.1%}" # '85.7%'
# Números grandes com vírgulas
population = 1234567
formatted_population = f"{population:,}" # '1,234,567'
# Números grandes com agrupamento por localidade usando `:n`
import locale
locale.setlocale(locale.LC_ALL, '') # Define para o local padrão do usuário
large_number = 1234567.89
formatted_number = f"{large_number:n}" # Ex: '1.234.567,89' ou '1,234,567.89'
Type Hints (Dicas de Tipo)
Use dicas de tipo para tornar o código mais legível e fácil de manter. Elas ajudam IDEs a fornecer autocompletar, detectar erros cedo e servem como documentação inline.
Sim
def calculate_total(price: float, quantity: int) -> float:
"""Calcula o preço total dos itens."""
return price * quantity
def greet(name: str) -> str:
"""Retorna uma mensagem de saudação."""
return f"Olá, {name}!"
def process_items(items: list[str]) -> dict[str, int]:
"""Conta ocorrências de cada item."""
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
return counts
Não
def calculate_total(price, quantity):
"""Calcula o preço total dos itens."""
return price * quantity # Quais tipos? Vai funcionar com qualquer entrada?
def greet(name):
"""Retorna uma mensagem de saudação."""
return f"Olá, {name}!" # name é sempre string?
def process_items(items):
"""Conta ocorrências de cada item."""
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
return counts # O que retorna?
Racional: Dicas de tipo melhoram clareza, permitem melhor suporte de IDE (autocomplete, refatoração, detecção de erros) e ajudam a encontrar bugs antes da execução. IDEs modernas como VS Code e PyCharm usam type hints para sugestões inteligentes e avisos.
Dicas de tipo modernas (Python 3.9+)
A partir do Python 3.9, você pode usar tipos embutidos diretamente ao invés de importar do typing. O Python 3.10+ também introduz o operador | para tipos união.
Sim (Python 3.10+)
# Use tipos embutidos com [] (Python 3.9+)
def process_items(items: list[str]) -> dict[str, int]:
"""Conta ocorrências de cada item."""
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
return counts
# Use operador | para uniões (Python 3.10+)
def find_user(user_id: int) -> str | None:
"""Busca usuário por ID, retorna None se não encontrar."""
if user_id > 0:
return "Alice"
return None
def process_id(id_value: int | str) -> str:
"""Processa ID que pode ser int ou string."""
return str(id_value)
# Múltiplos tipos união
def parse_config(value: str | int | float | None) -> str:
"""Processa valor de configuração."""
return str(value) if value is not None else ""
Estilo antigo (Python 3.5-3.8)
from typing import Optional, Union, List, Dict
# Era necessário importar e usar tipos genéricos com letra maiúscula
def process_items(items: List[str]) -> Dict[str, int]:
"""Conta ocorrências de cada item."""
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
return counts
# Usava Optional e Union do módulo typing
def find_user(user_id: int) -> Optional[str]:
"""Busca usuário por ID, retorna None se não encontrar."""
if user_id > 0:
return "Alice"
return None
def process_id(id_value: Union[int, str]) -> str:
"""Processa ID que pode ser int ou string."""
return str(id_value)
Nota: Se estiver usando Python 3.10 ou superior, prefira a sintaxe moderna com | e tipos embutidos (list, dict, set, tuple). Para Python 3.9, use tipos embutidos com [] mas continue usando Optional e Union do typing. O estilo antigo com List, Dict etc. do typing está depreciado, mas ainda funciona.
Mais exemplos de type hints
from typing import Any, Callable
# Dicas de tipo em classes
class User:
def __init__(self, name: str, age: int) -> None:
self.name: str = name
self.age: int = age
def get_info(self) -> dict[str, Any]:
"""Retorna informações do usuário."""
return {"name": self.name, "age": self.age}
# Dicas de tipo para funções
def apply_operation(x: int, operation: Callable[[int], int]) -> int:
"""Aplica uma função a um número."""
return operation(x)
# Alias de tipo para tipos complexos
UserId = int
UserData = dict[str, str | int] # Python 3.10+
# Ou para versões antigas: dict[str, Union[str, int]]
def get_user_data(user_id: UserId) -> UserData:
"""Obtém dados do usuário pelo ID."""
return {"name": "Alice", "age": 30}
Benefícios para IDE e assistentes de IA
Com type hints, sua IDE e assistentes de código podem:
- Autocomplete: Sugerir métodos e atributos baseado no tipo
- Detecção de erros: Avisar ao passar tipos errados antes de rodar
- Refatoração: Renomear variáveis e funções com segurança
- Documentação: Mostrar tipos nos parâmetros sem ler documentação
- Assistência de IA: GitHub Copilot, Codeium e outros sugerem melhor quando entendem seus tipos
# IDE sabe que 'result' é float, sugere métodos de float
result: float = calculate_total(19.99, 3)
result.is_integer() # IDE sugere este método
# IDE avisa se passar tipos errados
calculate_total("19.99", "3") # IDE mostra aviso: esperado float e int
# Assistentes de IA sugerem melhor com type hints
def process_users(users: list[dict[str, str]]) -> list[str]:
# IA sabe que 'users' é lista de dicionários, sugere operações apropriadas
# IA sabe que retorno deve ser lista de strings
return [user["name"] for user in users] # IA sugere corretamente
Nota: Assistentes de IA como GitHub Copilot usam type hints para entender a intenção do código e sugerir de forma mais precisa e contextual. Código bem tipado recebe melhor assistência.
Aproveite a biblioteca padrão
A biblioteca padrão do Python é extensa e bem testada. Antes de escrever soluções customizadas ou instalar pacotes de terceiros, verifique se a biblioteca padrão já oferece o que você precisa. Conhecer os módulos comuns vai te tornar um programador Python mais eficiente.
collections - Tipos de containers especializados
O módulo collections oferece alternativas aos containers da linguagem com funcionalidades extras.
Counter - Contador de objetos hashable
Sim
from collections import Counter
# Conta ocorrências em uma lista
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
word_counts = Counter(words)
print(word_counts) # Counter({'apple': 3, 'banana': 2, 'orange': 1})
# Obtém os itens mais comuns
print(word_counts.most_common(2)) # [('apple', 3), ('banana', 2)]
# Combina contadores
more_words = ["apple", "grape"]
word_counts.update(more_words)
Não
# Contando manualmente (verboso e propenso a erro)
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
word_counts = {}
for word in words:
if word in word_counts:
word_counts[word] += 1
else:
word_counts[word] = 1
defaultdict - Dicionário com valores padrão
from collections import defaultdict
# Agrupa itens por categoria
items = [("fruta", "maçã"), ("fruta", "banana"), ("legume", "cenoura")]
grouped = defaultdict(list)
for category, item in items:
grouped[category].append(item)
# {'fruta': ['maçã', 'banana'], 'legume': ['cenoura']}
functools - Funções de ordem superior
partial - Aplicação parcial de função
Sim
from functools import partial
def power(base: int, exponent: int) -> int:
"""Eleva base à potência de exponent."""
return base ** exponent
# Cria funções especializadas
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
Não
# Criando funções wrapper manualmente
def power(base: int, exponent: int) -> int:
"""Eleva base à potência de exponent."""
return base ** exponent
def square(base: int) -> int:
return power(base, 2)
def cube(base: int) -> int:
return power(base, 3)
lru_cache - Decorador de memoização
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
"""Calcula o n-ésimo número de Fibonacci com cache."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Muito mais rápido para chamadas repetidas
print(fibonacci(100)) # Calculado instantaneamente devido ao cache
itertools - Ferramentas para iteradores
chain - Combina múltiplos iteráveis
from itertools import chain
# Combina múltiplas listas em uma
lista1 = [1, 2, 3]
lista2 = [4, 5, 6]
combinada = list(chain(lista1, lista2)) # [1, 2, 3, 4, 5, 6]
combinations - Todas as combinações de itens
from itertools import combinations
# Obtém todas as combinações de 2 itens
itens = ['A', 'B', 'C']
combos = list(combinations(itens, 2))
# [('A', 'B'), ('A', 'C'), ('B', 'C')]
Para mais funções de combinatória, consulte a documentação oficial do Python.
groupby - Agrupa itens consecutivos
from itertools import groupby
# Agrupa itens consecutivos por uma chave
dados = [('A', 1), ('A', 2), ('B', 1), ('B', 2), ('A', 3)]
for chave, grupo in groupby(dados, key=lambda x: x[0]):
print(f"{chave}: {list(grupo)}")
# A: [('A', 1), ('A', 2)]
# B: [('B', 1), ('B', 2)]
# A: [('A', 3)]
islice - Fatia um iterador
from itertools import islice
# Obtém itens de um iterador sem carregar tudo na memória
def gera_numeros():
n = 0
while True:
yield n
n += 1
# Obtém os primeiros 10 números pares
pares = (x for x in gera_numeros() if x % 2 == 0)
primeiros_dez_pares = list(islice(pares, 10)) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
pathlib - Manipulação orientada a objetos de caminhos
Sim
from pathlib import Path
# Operações modernas e legíveis com caminhos
projeto = Path("/home/usuario/projeto")
arquivo_config = projeto / "config" / "settings.json"
if arquivo_config.exists():
conteudo = arquivo_config.read_text()
# Lista todos os arquivos Python
arquivos_py = list(projeto.glob("**/*.py"))
Não
import os
# Manipulação de strings antiga
projeto = "/home/usuario/projeto"
arquivo_config = os.path.join(projeto, "config", "settings.json")
if os.path.exists(arquivo_config):
with open(arquivo_config, 'r') as f:
conteudo = f.read()
dataclasses - Reduz o boilerplate de classes
Sim (Python 3.7+)
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3.0, 4.0)
print(p) # Point(x=3.0, y=4.0)
print(p.distance_from_origin()) # 5.0
Não
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
Racional: A biblioteca padrão é bem documentada, testada e não exige dependências extras. Aprender esses módulos te ajuda a escrever código mais conciso, eficiente e idiomático.
Recursos Modernos do Python
O Python continua a evoluir com cada versão, introduzindo novas sintaxes e recursos que tornam o código mais legível e conciso. Aqui estão algumas adições significativas das versões recentes do Python.
Expressões de atribuição (Walrus Operator) - Python 3.8+
O operador walrus (morsa do inglês) := permite atribuir valores a variáveis como parte de uma expressão. Útil em condições e loops.
Sim
# Atribuir e usar em uma linha
if (n := len(dados)) > 10:
print(f"A lista é muito longa ({n} elementos, esperado <= 10)")
# Evita chamadas redundantes em while loops
while (linha := arquivo.readline()) != "":
processa(linha)
# List comprehensions com subexpressão compartilhada
dados = [1, 2, 3, 4, 5]
resultados = [y for x in dados if (y := x * 2) > 5] # [6, 8, 10]
# Reutiliza computação cara em comprehension
resultados_caros = [resultado for x in dados
if (resultado := funcao_cara(x)) is not None]
Não
# Sem walrus operator - menos conciso
n = len(dados)
if n > 10:
print(f"A lista é muito longa ({n} elementos, esperado <= 10)")
# Chamadas redundantes
linha = arquivo.readline()
while linha != "":
processa(linha)
linha = arquivo.readline()
# Computando duas vezes
resultados = [x * 2 for x in dados if x * 2 > 5]
Racional: O operador walrus reduz duplicação de código e deixa a intenção mais clara, especialmente quando você precisa usar um valor computado múltiplas vezes.
Parâmetros posicionais e apenas-por-palavra-chave (Python 3.8+)
Use / para especificar parâmetros que devem ser passados apenas por posição, e * para parâmetros que devem ser passados apenas por palavra-chave. Isso torna a API mais explícita e evita usos indevidos.
Sim
# Parâmetros apenas-posicionais (antes de /)
def saudacao(nome, /, cumprimento="Olá"):
"""`nome` deve ser posicional, `cumprimento` pode ser palavra-chave."""
return f"{cumprimento}, {nome}!"
saudacao("Alice") # OK
saudacao("Alice", cumprimento="Oi") # OK
# saudacao(nome="Alice") # Erro: nome é apenas-posicional
# Parâmetros apenas-por-palavra-chave (após *)
def cria_usuario(username, *, email, idade):
"""`email` e `idade` devem ser passados como keywords."""
return {"username": username, "email": email, "idade": idade}
cria_usuario("alice", email="alice@example.com", idade=30) # OK
# cria_usuario("alice", "alice@example.com", 30) # Erro: email e idade devem ser keyword
# Combinando ambos
def processa(a, b, /, c, *, d, e):
"""a, b são apenas-posicionais; c pode ser ambos; d, e são apenas-keyword."""
pass
Não
# API ambígua sem restrições de parâmetros
def saudacao(nome, cumprimento="Olá"):
return f"{cumprimento}, {nome}!"
# Usuários podem fazer isso (confuso):
saudacao(cumprimento="Alice", nome="Hello") # Confuso!
Racional: Tipos explícitos de parâmetros evitam usos indevidos da API e tornam as assinaturas de funções autodocumentadas.
Pattern Matching (match/case) - Python 3.10+
A instrução match/case fornece correspondência estrutural poderosa, útil para substituir cadeias longas de if/elif ao lidar com dados estruturados.
Sim
# Correspondência de valor simples
def http_status(status: int) -> str:
match status:
case 200:
return "OK"
case 404:
return "Não encontrado"
case 500:
return "Erro interno do servidor"
case _:
return "Status desconhecido"
# Pattern matching com estruturas
def processa_comando(comando: tuple) -> str:
match comando:
case ("sair",):
return "Saindo..."
case ("carregar", arquivo):
return f"Carregando {arquivo}"
case ("salvar", arquivo, formato):
return f"Salvando {arquivo} como {formato}"
case _:
return "Comando desconhecido"
# Correspondência de objetos e tipos
def descreve(obj):
match obj:
case int(x) if x > 0:
return f"Inteiro positivo: {x}"
case int(x) if x < 0:
return f"Inteiro negativo: {x}"
case str(s) if len(s) > 0:
return f"String não-vazia: {s}"
case list([]):
return "Lista vazia"
case list([primeiro, *resto]):
return f"Lista começando com {primeiro}"
case {"name": nome, "age": idade}:
return f"Pessoa: {nome}, idade {idade}"
case _:
return "Algo mais"
Não
# Cadeias longas de if/elif (menos legível)
def http_status(status: int) -> str:
if status == 200:
return "OK"
elif status == 404:
return "Não encontrado"
elif status == 500:
return "Erro interno do servidor"
else:
return "Status desconhecido"
def processa_comando(comando: tuple) -> str:
if len(comando) == 1 and comando[0] == "sair":
return "Saindo..."
elif len(comando) == 2 and comando[0] == "carregar":
return f"Carregando {comando[1]}"
elif len(comando) == 3 and comando[0] == "salvar":
return f"Salvando {comando[1]} como {comando[2]}"
else:
return "Comando desconhecido"
Racional: Pattern matching torna a lógica condicional complexa mais legível e fácil de manter, especialmente ao lidar com dados estruturados.
Operadores de mesclar e atualizar para dicionários (Python 3.9+)
Use | e |= para mesclar dicionários de forma clara.
Sim
# Mesclar dicionários com |
defaults = {"cor": "azul", "tamanho": "médio"}
custom = {"tamanho": "grande", "estilo": "moderno"}
config = defaults | custom # {'cor': 'azul', 'tamanho': 'grande', 'estilo': 'moderno'}
# Atualiza na própria variável com |=
settings = {"tema": "escuro"}
settings |= {"idioma": "pt", "tema": "claro"}
# settings agora é {'tema': 'claro', 'idioma': 'pt'}
Não
# Maneira antiga - mais verbosa
defaults = {"cor": "azul", "tamanho": "médio"}
custom = {"tamanho": "grande", "estilo": "moderno"}
config = {**defaults, **custom} # Funciona mas é menos claro
# Ou cópia + update
config = defaults.copy()
config.update(custom)
Racional: O operador | torna a mesclagem de dicionários mais intuitiva e consistente com operações de conjuntos.
Mensagens de erro melhores (Python 3.10+)
Versões recentes do Python apresentam mensagens de erro com mais contexto e sugestões — aproveite isso ao depurar e ao escrever testes.
# Exemplo: Melhoria de sintaxe de mensagens de erro
# Python 3.10+ apontará para a localização exata e sugerirá correções
# Dois pontos ausentes
# if x > 5
# ^
# SyntaxError: expected ':'
# Melhores sugestões de NameError
name = "Alice"
# print(nam)
# NameError: name 'nam' is not defined. Did you mean: 'name'?
# Melhores AttributeErrors
class User:
def __init__(self):
self.username = "alice"
user = User()
# print(user.usrname)
# AttributeError: 'User' object has no attribute 'usrname'. Did you mean: 'username'?
Context managers entre parênteses (Python 3.10+)
Permitem escrever múltiplos context managers com quebras de linha naturais.
Sim
with (
open("entrada.txt") as fin,
open("saida.txt", "w") as fout,
open("log.txt", "w") as logfile,
):
processa_arquivos(fin, fout, logfile)
Não
# Continuação com barra invertida menos legível
with open("entrada.txt") as fin, \
open("saida.txt", "w") as fout, \
open("log.txt", "w") as logfile:
processa_arquivos(fin, fout, logfile)
Racional: Context managers entre parênteses melhoram a legibilidade ao trabalhar com múltiplos recursos.
Expressões idiomáticas Python essenciais e boas práticas
O padrão if __name__ == "__main__"
Use esta expressão para tornar seus arquivos Python tanto módulos importáveis quanto scripts executáveis.
Sim
# meumodulo.py
def calcula_soma(a: int, b: int) -> int:
"""Adiciona dois números."""
return a + b
def main():
"""Ponto de entrada quando executado como script."""
resultado = calcula_soma(5, 3)
print(f"Resultado: {resultado}")
if __name__ == "__main__":
main()
Quando importado:
# outro_arquivo.py
import meumodulo
# Apenas a função está disponível, main() não roda automaticamente
resultado = meumodulo.calcula_soma(10, 20)
Quando executado diretamente:
python meumodulo.py # Roda main() e imprime "Resultado: 8"
Não
# meumodulo.py - Má prática
def calcula_soma(a: int, b: int) -> int:
"""Adiciona dois números."""
return a + b
# Código roda imediatamente quando importado (ruim!)
resultado = calcula_soma(5, 3)
print(f"Resultado: {resultado}")
Racional: Este padrão permite reutilização de código. O mesmo arquivo pode ser usado como módulo importável ou executado como um script autônomo sem efeitos colaterais indesejados durante a importação.
Logging vs Print
Use o módulo logging ao invés de print() para qualquer coisa além de scripts simples. Logging fornece melhor controle sobre níveis de saída, formatação e destinos.
Sim
import logging
# Configura logging no início da sua aplicação
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def processa_dados(dados):
logger.debug(f"Processando dados: {dados}")
if not dados:
logger.warning("Dados vazios recebidos")
return None
try:
resultado = operacao_cara(dados)
logger.info(f"Processados {len(dados)} itens com sucesso")
return resultado
except Exception as e:
logger.error(f"Falha ao processar dados: {e}", exc_info=True)
raise
# Benefícios:
# - Pode mudar o nível de log sem modificar código
# - Logs incluem timestamps e contexto
# - Pode redirecionar para arquivos, streams ou serviços externos
# - Diferentes loggers para diferentes módulos
Não
def processa_dados(dados):
print(f"Processando dados: {dados}") # Sempre imprime, sem controle
if not dados:
print("AVISO: Dados vazios recebidos") # Sem formato padrão
return None
try:
resultado = operacao_cara(dados)
print(f"Processados {len(dados)} itens") # Misto com saída real
return resultado
except Exception as e:
print(f"ERRO: {e}") # Sem stack trace, difícil depurar
raise
Níveis de log
import logging
logger = logging.getLogger(__name__)
# Use níveis apropriados
logger.debug("Informação detalhada para depuração")
logger.info("Mensagens informativas gerais")
logger.warning("Avisos para situações inesperadas")
logger.error("Erros para problemas sérios")
logger.critical("Erros críticos para problemas muito graves")
# Em produção, defina o nível para INFO ou WARNING para reduzir ruído
logging.basicConfig(level=logging.WARNING)
# Agora apenas avisos, erros e mensagens críticas aparecem
Racional: Logging é configurável, estruturado e pronto para produção. Permite controlar verbosidade sem mudar código e fornece melhor contexto para depuração.
Operações eficientes em coleções
Escolha a estrutura de dados correta para o trabalho. Entender características de performance evita código ineficiente. Para mais casos de uso, consulte este post de blog.
Testando membros em conjuntos
Sim
# Use conjuntos para testagem de membros - O(1) caso médio
usuarios_validos = {"alice", "bob", "charlie", "david"} # set
if username in usuarios_validos: # Rápido: O(1)
concede_acesso()
# Converta lista para conjunto para múltiplas buscas
lista_usuarios = ["alice", "bob", "charlie", "david"]
conjunto_usuarios = set(lista_usuarios) # Converte uma vez
for tentativa_login in tentativas_login:
if tentativa_login in conjunto_usuarios: # O(1) por verificação
processa_login(tentativa_login)
Não
# Usar listas para testagem de membros - O(n) busca linear
usuarios_validos = ["alice", "bob", "charlie", "david"] # list
if username in usuarios_validos: # Lento: O(n)
concede_acesso()
# Buscar repetidamente em listas
for tentativa_login in tentativas_login:
if tentativa_login in usuarios_validos: # O(n) por verificação - muito lento!
processa_login(tentativa_login)
Dicionário para buscas rápidas
Sim
# Use dicionários para buscas chave-valor - O(1)
scores_usuarios = {
"alice": 95,
"bob": 87,
"charlie": 92
}
score = scores_usuarios.get("alice", 0) # Busca rápida com padrão
# Dict comprehension
ao_quadrado = {x: x**2 for x in range(10)}
# {0: 0, 1: 1, 2: 4, 3: 9, ...}
Não
# Usar listas paralelas - ineficiente e propenso a erros
nomes_usuarios = ["alice", "bob", "charlie"]
scores = [95, 87, 92]
# Busca linear para encontrar score - O(n)
def get_score(nome_usuario):
for i, nome in enumerate(nomes_usuarios):
if nome == nome_usuario:
return scores[i]
return 0
Operações com conjuntos
# Operações com conjuntos são rápidas e legíveis
set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}
# União - todos os elementos únicos
uniao = set_a | set_b # {1, 2, 3, 4, 5, 6, 7, 8}
# Interseção - elementos comuns
intersecao = set_a & set_b # {4, 5}
# Diferença - elementos em a mas não em b
diferenca = set_a - set_b # {1, 2, 3}
# Diferença simétrica - elementos em um ou outro mas não em ambos
diff_sim = set_a ^ set_b # {1, 2, 3, 6, 7, 8}
# Remove duplicatas de uma lista
itens_unicos = list(set(itens_com_duplicatas))
Lista vs Gerador para dados grandes
Sim
# Gerador - eficiente em memória para dados grandes
def le_arquivo_grande(nome_arquivo):
with open(nome_arquivo) as f:
for linha in f: # Processa uma linha por vez
yield linha.strip()
# Carrega apenas uma linha por vez
for linha in le_arquivo_grande("arquivo_gigante.txt"):
processa(linha)
# Expressão de gerador
soma_de_quadrados = sum(x**2 for x in range(1000000)) # Eficiente em memória
Não
# Lista - carrega tudo em memória
def le_arquivo_grande(nome_arquivo):
with open(nome_arquivo) as f:
return [linha.strip() for linha in f] # Carrega arquivo inteiro!
# Usa muita memória
todas_linhas = le_arquivo_grande("arquivo_gigante.txt")
for linha in todas_linhas:
processa(linha)
# List comprehension - cria lista completa em memória
soma_de_quadrados = sum([x**2 for x in range(1000000)]) # Desperdiçador
Racional: Usar a estrutura de dados correta melhora a performance dramaticamente. Conjuntos e dicionários oferecem buscas O(1), enquanto listas requerem O(n). Geradores economizam memória para datasets grandes.
Duck Typing e EAFP vs LBYL
Duck typing é um conceito de programação onde o tipo ou classe de um objeto é determinado pelo seu comportamento (métodos e propriedades), ao invés de sua herança ou tipo explícito. A frase “Se caminha como um pato e faz quac como um pato, é um pato” significa que, se um objeto implementa os métodos ou comportamentos necessários, ele pode ser usado onde esses comportamentos são esperados — independentemente do seu tipo real. Essa abordagem torna o código mais flexível e reutilizável, pois foca no que um objeto pode fazer, não no que ele é. Python também segue a filosofia de “É mais fácil pedir perdão do que permissão” (EAFP) em vez de “Olhe antes de pular” (LBYL).
EAFP - Abordagem Pythônica
Sim
# EAFP: Tenta a operação, trata exceções se falhar
def get_user_age(user_dict):
try:
return user_dict["age"]
except KeyError:
return None
# Duck typing: "Se caminha como um pato e faz quac como um pato..."
def processa_arquivo(file_obj):
# Não verifica tipo - apenas usa como arquivo
try:
conteudo = file_obj.read()
return conteudo.upper()
except AttributeError:
raise TypeError("Objeto deve ter método read()")
# Funciona com qualquer objeto tipo-arquivo
from io import StringIO
processa_arquivo(open("file.txt")) # Arquivo real
processa_arquivo(StringIO("teste")) # Buffer de string - também funciona!
# EAFP com acesso a dict
config = {"timeout": 30, "retries": 3}
try:
timeout = config["timeout"]
retries = config["retries"]
except KeyError as e:
raise ValueError(f"Config obrigatória faltando: {e}")
Não - LBYL (Olhe antes de pular)
# LBYL: Verifica antes de tentar (menos Pythônico, race conditions)
def get_user_age(user_dict):
if "age" in user_dict: # Verificação extra
return user_dict["age"]
else:
return None
# Verificação de tipo ao invés de duck typing (rígido)
def processa_arquivo(file_obj):
if not isinstance(file_obj, io.IOBase): # Muito restritivo!
raise TypeError("Deve ser um objeto arquivo")
conteudo = file_obj.read()
return conteudo.upper()
# Agora StringIO não funciona, mesmo tendo read()!
# Múltiplas verificações (verboso e mais lento)
config = {"timeout": 30, "retries": 3}
if "timeout" in config and "retries" in config:
timeout = config["timeout"]
retries = config["retries"]
else:
raise ValueError("Config obrigatória faltando")
Mais exemplos de EAFP
# Convertendo para int
# EAFP - Pythônico
try:
valor = int(entrada_usuario)
except ValueError:
print("Número inválido")
# LBYL - Não Pythônico
if entrada_usuario.isdigit():
valor = int(entrada_usuario)
else:
print("Número inválido")
# Problema: isdigit() não lida com números negativos ou floats
# Operações com arquivo
# EAFP - Pythônico
try:
with open("config.json") as f:
config = json.load(f)
except FileNotFoundError:
config = config_padrao()
except json.JSONDecodeError:
print("JSON inválido")
# LBYL - Menos Pythônico
import os
if os.path.exists("config.json"):
# Race condition: arquivo pode ser deletado entre verificação e abertura!
with open("config.json") as f:
config = json.load(f)
else:
config = config_padrao()
Benefícios do Duck Typing
# Funções funcionam com qualquer tipo compatível
def imprime_tudo(itens):
"""Funciona com listas, tuplas, conjuntos, geradores, etc."""
for item in itens: # Apenas precisa ser iterável
print(item)
imprime_tudo([1, 2, 3]) # lista
imprime_tudo((1, 2, 3)) # tupla
imprime_tudo({1, 2, 3}) # conjunto
imprime_tudo(range(1, 4)) # objeto range
imprime_tudo(x for x in [1,2,3]) # gerador
# Classe customizada também funciona se implementa __iter__
class MinhaColecao:
def __init__(self, dados):
self.dados = dados
def __iter__(self):
return iter(self.dados)
imprime_tudo(MinhaColecao([1, 2, 3])) # Funciona!
Racional: EAFP é mais Pythônico, geralmente mais rápido (uma operação vs verificação + operação), e lida com casos extremos melhor. Duck typing torna código mais flexível e reutilizável ao focar em comportamento ao invés de tipo.
Gostou de Ler este Artigo?
Aqui estão alguns artigos relacionados que você pode gostar de ler: