Boas Práticas em Automações Python
Abaixo, iremos destacar as principais práticas recomendadas para você desenvolver um código de qualidade, resiliente e seguro. Iremos indicar as PEPs (acrônimo para Python Enhancements Proposal) para garantir qualidade e conformidade no seu código, juntamente com exemplos de snippets de código aplicando as recomendações especificadas nas PEPs.
1. PEP 8 - Estilo de Código
Define convenções de nomeação e formatação para código legível.
Regras principais:
- Variáveis e funções:
snake_case - Classes:
PascalCase - Constantes:
UPPER_SNAKE_CASE - Indentação: 4 espaços
- Máximo de 79 caracteres por linha
# ✓ Correto
def processar_dados_usuario(usuario_id: int) -> dict:
"""Processa dados do usuário."""
MAX_TENTATIVAS = 3
return {}
# ✗ Incorreto
def ProcessarDadosUsuario(usuarioId):
maxTentativas = 3
return {}
2. PEP 257 - Docstrings
Define padrão para documentação de código, indicando a inserção de DocStrings para explicar parâmetros e papel desempenhado por uma função, classe ou até mesmo arquivo.
def conectar_banco(host: str, usuario: str, senha: str) -> Connection:
"""
Conecta ao banco de dados PostgreSQL.
Args:
host: Endereço do servidor
usuario: Usuário do banco
senha: Senha do banco
Returns:
Conexão ativa com o banco
Raises:
ConnectionError: Se falhar na conexão
"""
pass
class ProcessadorDados:
"""Processa e valida dados de entrada."""
def executar(self) -> None:
"""Executa o processamento."""
pass
3. PEP 484/585 - Type Hints
Define anotações de tipo estáticas para melhorar legibilidade e detectar erros.
from typing import Optional, Callable
from collections.abc import Sequence
# Tipos básicos
def buscar_usuarios(ids: list[int]) -> dict[int, str]:
"""Busca usuários por IDs."""
pass
# Tipos opcionais
def enviar_email(destinatario: str, assunto: Optional[str] = None) -> bool:
"""Envia e-mail com assunto opcional."""
pass
# Callable para funções
def processar_com_callback(dados: list, callback: Callable[[str], None]) -> None:
"""Processa dados executando callback."""
pass
# Sequence para qualquer sequência
def filtrar_dados(registros: Sequence[dict], chave: str) -> list[dict]:
"""Filtra registros por chave."""
pass
4. PEP 586 - TypedDict
Define dicionários com tipos específicos para cada chave.
from typing import TypedDict
class UsuarioDict(TypedDict):
"""Estrutura de usuário."""
id: int
nome: str
email: str
ativo: bool
def processar_usuario(usuario: UsuarioDict) -> str:
"""Processa usuário tipado."""
return f"{usuario['nome']} - {usuario['email']}"
dados: UsuarioDict = {
'id': 1,
'nome': 'João',
'email': 'joao@example.com',
'ativo': True
}
5. PEP 3134 - Tratamento de Exceções
Define como encadear exceções preservando contexto original. Exceções claras ajudam a entender qual é o comportamento que está causando falhas no código, reduzindo o tempo de ajuste e garantindo que a automação seja colocada em produção novamente o quanto antes.
import logging
logger = logging.getLogger(__name__)
# ✗ Incorreto - perde contexto
try:
resultado = api.chamar("/dados")
except Exception:
raise RuntimeError("Erro na API")
# ✓ Correto - preserva contexto
try:
resultado = api.chamar("/dados")
except ConnectionError as e:
logger.error(f"Falha na conexão: {e}", exc_info=True)
raise RuntimeError("Serviço indisponível") from e
# Sempre trate exceções específicas
try:
conexao.conectar()
except ConnectionError as e:
logger.error(f"Erro de conexão: {e}")
raise
except TimeoutError as e:
logger.warning(f"Timeout: {e}")
# Implementar retry
except Exception as e:
logger.critical(f"Erro inesperado: {e}", exc_info=True)
raise
6. PEP 391 - Logging Configurado
Define logging estruturado ao invés de print(). Logging é uma ferramenta excelente para identificar precedentes de comportamentos errôneos, e podem ser conectados a softwares de observabilidade, ajudando a prevenir erros e downtime na sua automação.
import logging
import logging.config
config = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'padrão': {
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'padrão',
},
'arquivo': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'logs/automacao.log',
'maxBytes': 10485760, # 10MB
'backupCount': 5,
'level': 'DEBUG',
'formatter': 'padrão',
},
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'arquivo'],
},
}
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
# Uso
logger.debug("Informação detalhada")
logger.info("Informação importante")
logger.warning("Aviso")
logger.error("Erro", exc_info=True)
logger.critical("Erro crítico")
7. Modularização - Separar por Responsabilidade
Organize código em módulos independentes e reutilizáveis. Utilize classes para separações lógicas e agregar funções para realizar uma mesma tarefa, e utilize pastas para segregar arquivos de acordo com seus respectivos papéis no contexto de um projeto de automação.
automacao/
├── src/
│ └── automacao/
│ ├── __init__.py
│ ├── db/
│ │ └── conexoes.py
│ ├── api/
│ │ └── cliente.py
│ ├── csv/
│ │ └── processador.py
│ ├── email/
│ │ └── gerenciador.py
│ └── utils/
│ ├── logger.py
│ └── validadores.py
└── tests/
Módulo de banco de dados:
# src/automacao/db/conexoes.py
import psycopg2
from typing import Optional
class ConexaoBD:
"""Gerencia conexões com PostgreSQL."""
def __init__(self, host: str, usuario: str, senha: str, banco: str):
self.host = host
self.usuario = usuario
self.senha = senha
self.banco = banco
self._conexao = None
def conectar(self):
try:
self._conexao = psycopg2.connect(
host=self.host,
user=self.usuario,
password=self.senha,
database=self.banco
)
logger.info(f"Conectado ao banco: {self.banco}")
except psycopg2.OperationalError as e:
logger.error(f"Erro de conexão: {e}", exc_info=True)
raise ConnectionError(f"Falha ao conectar: {e}") from e
def __enter__(self):
self.conectar()
return self._conexao
def __exit__(self, exc_type, exc_val, exc_tb):
if self._conexao:
self._conexao.close()
Módulo de CSV:
# src/automacao/csv/processador.py
import csv
from pathlib import Path
from typing import list
class ProcessadorCSV:
"""Processa arquivos CSV."""
def __init__(self, encoding: str = 'utf-8'):
self.encoding = encoding
def ler(self, arquivo: str) -> list[dict]:
"""Lê CSV e retorna lista de dicionários."""
caminho = Path(arquivo)
if not caminho.exists():
raise FileNotFoundError(f"Arquivo não encontrado: {arquivo}")
try:
with open(caminho, 'r', encoding=self.encoding) as f:
leitor = csv.DictReader(f)
return list(leitor)
except Exception as e:
logger.error(f"Erro ao ler CSV: {e}", exc_info=True)
raise
def escrever(self, arquivo: str, dados: list[dict]) -> None:
"""Escreve dados em arquivo CSV."""
if not dados:
raise ValueError("Lista de dados vazia")
try:
with open(arquivo, 'w', newline='', encoding=self.encoding) as f:
campos = dados[0].keys()
escritor = csv.DictWriter(f, fieldnames=campos)
escritor.writeheader()
escritor.writerows(dados)
logger.info(f"Arquivo escrito: {arquivo}")
except Exception as e:
logger.error(f"Erro ao escrever CSV: {e}", exc_info=True)
raise
Módulo de API:
# src/automacao/api/cliente.py
import requests
from typing import dict, Any
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class ClienteAPI:
"""Cliente HTTP com retry automático."""
def __init__(self, url_base: str, timeout: int = 30):
if not url_base.startswith(('http://', 'https://')):
raise ValueError("URL inválida")
self.url_base = url_base
self.timeout = timeout
self.sessao = self._criar_sessao()
def _criar_sessao(self) -> requests.Session:
"""Cria sessão com retry automático."""
sessao = requests.Session()
retry = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504]
)
adaptador = HTTPAdapter(max_retries=retry)
sessao.mount("http://", adaptador)
sessao.mount("https://", adaptador)
return sessao
def get(self, endpoint: str) -> dict[str, Any]:
"""Faz requisição GET."""
try:
url = f"{self.url_base}/{endpoint.lstrip('/')}"
resposta = self.sessao.get(url, timeout=self.timeout)
resposta.raise_for_status()
return resposta.json()
except requests.RequestException as e:
logger.error(f"Erro na requisição GET: {e}", exc_info=True)
raise
def post(self, endpoint: str, dados: dict) -> dict[str, Any]:
"""Faz requisição POST."""
try:
url = f"{self.url_base}/{endpoint.lstrip('/')}"
resposta = self.sessao.post(url, json=dados, timeout=self.timeout)
resposta.raise_for_status()
return resposta.json()
except requests.RequestException as e:
logger.error(f"Erro na requisição POST: {e}", exc_info=True)
raise
8. Validação de Entrada
Valide dados na entrada para evitar bugs. Utilize bibliotecas e recursos do Python para garantir que os dados de entrada estejam no formato esperado, evitando erros de manipulação de tipos de dados.
from dataclasses import dataclass
@dataclass
class ConfiguracaoAutomacao:
"""Configuração com validação."""
url: str
timeout: int = 30
retries: int = 3
def __post_init__(self):
if not self.url.startswith(('http://', 'https://')):
raise ValueError("URL deve ser válida")
if self.timeout <= 0:
raise ValueError("Timeout deve ser positivo")
if self.retries < 0:
raise ValueError("Retries deve ser não-negativo")
# Uso
try:
config = ConfiguracaoAutomacao(
url="https://api.example.com",
timeout=60,
retries=5
)
except ValueError as e:
logger.error(f"Configuração inválida: {e}")
raise
9. Configuração Centralizada
Use variáveis de ambiente e não hardcodes. Definindo variáveis de ambiente numa classe de configuração é uma forma de separar as responsabilidades das classes e garantimos que as configurações estejam definidas antes mesmo de manipular os logs, credenciais, conexões com banco de dados e afins.
import os
from pathlib import Path
class Config:
"""Configurações centralizadas."""
BASE_DIR = Path(__file__).parent.parent
# Banco de dados
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "user")
DB_PASS = os.getenv("DB_PASS", "password")
DB_NAME = os.getenv("DB_NAME", "database")
# API
API_URL = os.getenv("API_URL", "https://api.example.com")
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
# Logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
10. Testes Unitários
Escreva testes para validar funcionamentos e comportamentos. A Aplicação de testes unitários é uma prática fortemente recomendada, pois podemos checar se a lógica aplicada no programa está gerando resultados esperados antes mesmo do código entrar em produção, minimizando erros.
import pytest
from pathlib import Path
class TestProcessadorCSV:
"""Testes para ProcessadorCSV."""
def test_ler_csv_valido(self, tmp_path):
"""Testa leitura de CSV válido."""
arquivo = tmp_path / "teste.csv"
arquivo.write_text("nome,idade\nJoão,30\n")
processador = ProcessadorCSV()
dados = processador.ler(str(arquivo))
assert len(dados) == 1
assert dados[0]['nome'] == 'João'
def test_ler_csv_inexistente(self):
"""Testa erro ao ler arquivo inexistente."""
processador = ProcessadorCSV()
with pytest.raises(FileNotFoundError):
processador.ler("inexistente.csv")
def test_escrever_csv(self, tmp_path):
"""Testa escrita de CSV."""
arquivo = tmp_path / "saida.csv"
dados = [{'nome': 'João', 'idade': '30'}]
processador = ProcessadorCSV()
processador.escrever(str(arquivo), dados)
assert arquivo.exists()
assert 'João' in arquivo.read_text()
Saiba mais sobre boas-práticas:
Neste artigo, indicamos as principais boas-práticas recomendadas. Abaixo, iremos deixar materiais de consulta para vocês explorarem outras práticas recomendadas e demais casos de aplicação, além de ferramentas como Linters e Checagem de formatação.
Material de apoio
Conheça mais sobre as PEPs atuais do Python