Ir para o conteúdo

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