Todo projeto Python que leva qualidade de código a sério acaba acumulando um conjunto parecido de ferramentas no pyproject.toml: Flake8 para lint, isort para organizar imports, Black para formatação, Bandit para segurança, pyupgrade para modernizar sintaxe. Cada uma resolve um pedaço do problema — e cada uma também traz sua própria configuração, seu próprio tempo de execução, sua própria versão para manter compatível com as outras.
Rodar essas cinco ferramentas em sequência, especialmente num projeto grande, tem um custo real de tempo — cada uma percorre o código do zero, faz seu próprio parsing, e só então aplica suas regras. Multiplicado por cinco ferramentas, isso se acumula, principalmente em pipelines de CI que rodam a cada commit.
É esse problema específico que o Ruff resolve — não com mais uma ferramenta na pilha, mas substituindo a pilha inteira por uma única.
O que é o Ruff
O Ruff é um linter e formatador de código Python, criado pela Astral — a mesma empresa por trás do uv, o gerenciador de pacotes e ambientes que também vem ganhando espaço no ecossistema Python pelos mesmos motivos: performance muito acima do que a comunidade estava acostumada. Em vez de rodar várias ferramentas independentes, cada uma fazendo seu próprio parsing do código, o Ruff faz uma única passada pela árvore sintática (AST) do projeto e aplica todas as regras — lint, organização de import, segurança, modernização de sintaxe — nessa mesma passada.
Por que o Ruff é tão mais rápido
A resposta é a mesma que já vimos neste blog ao falar sobre o pydantic-core: o Ruff é escrito em Rust, não em Python. Isso, sozinho, já traria ganho de performance — código compilado tende a rodar mais rápido que código interpretado. Mas o ganho real vem de um detalhe arquitetural mais importante: ferramentas como Flake8, isort e Black, usadas em conjunto, cada uma faz seu próprio parsing do código Python do zero. Cinco ferramentas, cinco parsings independentes do mesmo arquivo. O Ruff faz o parsing uma única vez e aplica todas as regras equivalentes sobre essa mesma árvore sintática — eliminando o trabalho redundante que é a maior fonte de lentidão quando se empilha várias ferramentas de lint.
Na prática, isso significa que projetos que levavam dezenas de segundos (ou minutos, em bases de código grandes) para passar por todo o pipeline de lint e formatação passam a levar frações de segundo com o Ruff.
Instalando
Como a maior parte do ecossistema Python moderno, o Ruff pode ser instalado de várias formas, dependendo do seu fluxo de trabalho:
# Usando pip
pip install ruff
# Usando uv, de forma global (instala o binário no sistema)
uv tool install ruff
# Usando uv, como dependência de desenvolvimento do projeto
uv add --dev ruff
# Usando pipx
pipx install ruff
Vale uma nota sobre a diferença entre as duas formas com uv: uv tool install instala o Ruff globalmente, disponível em qualquer projeto — útil para uso pessoal no dia a dia. uv add --dev adiciona o Ruff como dependência de desenvolvimento daquele projeto específico, versionada no pyproject.toml — a forma recomendada em projetos de equipe, para garantir que todo mundo (e o pipeline de CI) rode exatamente a mesma versão.
Se você usa VS Code ou PyCharm, vale instalar a extensão oficial do Ruff — ela formata o código e corrige o que for automaticamente corrigível a cada salvamento, sem precisar abrir o terminal manualmente.
Antes de alterar qualquer coisa: diagnosticando o projeto
Ao entrar num projeto existente — especialmente um com bastante código legado —, o primeiro impulso de rodar ruff check --fix . direto pode não ser o mais prudente. É mais seguro primeiro entender o tamanho do problema:
ruff check . --statistics
Esse comando gera um relatório de quantas ocorrências existem de cada tipo de regra, sem alterar nenhuma linha de código. Isso tem valor prático em três frentes:
- Priorização. Se o relatório mostrar 200 ocorrências de "variável não utilizada" e 5 de "vulnerabilidade de segurança", a ordem de prioridade fica óbvia — segurança primeiro, mesmo sendo numericamente menor.
- Acompanhamento ao longo do tempo. Rodar o comando hoje, guardar o resultado, e rodar de novo depois de uma sprint de refatoração permite mostrar, com números concretos, quanto a qualidade do código avançou — útil inclusive para justificar tempo investido em débito técnico para quem está fora do time de engenharia.
- Zero risco. Como não altera nada, pode ser rodado livremente, inclusive por quem ainda está se familiarizando com o projeto.
Os três comandos do dia a dia
O uso cotidiano do Ruff gira em torno de três comandos:
# 1. Apenas verificar, sem alterar nada
ruff check .
# 2. Corrigir automaticamente o que for seguro corrigir
ruff check --fix .
# 3. Formatar o código (equivalente ao que o Black fazia)
ruff format
O --fix resolve automaticamente uma boa parte dos problemas comuns — imports desorganizados, sintaxe antiga que já tem equivalente mais moderno, entre outros —, mas não tudo: correções que podem alterar o comportamento do código (não só sua forma) exigem confirmação explícita com --unsafe-fixes, justamente para não aplicar, sem revisão, uma mudança que vá além de estilo.
O que o Ruff cobre, na prática
O Ruff não se limita a apontar erro de digitação ou espaçamento — ele reimplementa, em Rust, o conjunto de regras de várias ferramentas que a comunidade Python já usava separadamente:
- Detecção de erros lógicos — variáveis declaradas e nunca usadas, imports esquecidos, comparações sempre falsas. Esse conjunto de regras corresponde ao que o
Pyflakesfazia isoladamente. - Estilo, seguindo a PEP 8 — espaçamento, comprimento de linha, convenções de nomenclatura. É o equivalente ao que
pycodestylecobria, e que dá origem direto à PEP 8, o guia de estilo oficial do Python. - Organização de imports — equivalente ao que o
isortfazia como ferramenta separada. - Modernização de sintaxe — sugestões para trocar construções antigas por equivalentes mais modernos disponíveis em versões recentes do Python (por exemplo,
Union[int, str]porint | strem projetos que já suportam a sintaxe da PEP 604). Esse conjunto é equivalente ao que opyupgradefazia. - Segurança — detecção de padrões conhecidos como arriscados, como uso de
eval, senhas hardcoded, ou funções com histórico de vulnerabilidade. Esse conjunto corresponde ao que oBanditcobria isoladamente.
Cada uma dessas frentes é identificada por um prefixo de letras nas regras — e é justamente essa nomenclatura que aparece na configuração do projeto, na seção seguinte.
Configurando no pyproject.toml
O Ruff é configurado, na grande maioria dos projetos modernos, dentro do próprio pyproject.toml — mantendo a configuração de ferramentas de desenvolvimento centralizada num único arquivo, em vez de espalhada entre .flake8, .isort.cfg, pyproject.toml (para o Black) e .bandit, como era comum antes.
[tool.ruff]
# Comprimento máximo de linha
line-length = 79
# Diretórios e arquivos que o Ruff deve ignorar por completo
exclude = [
".venv",
"migrations",
"__init__.py",
]
[tool.ruff.lint]
# Prefixos de regras habilitadas — cada letra corresponde a uma
# das ferramentas que o Ruff substitui
select = [
"F", # Pyflakes — erros lógicos, variáveis não utilizadas
"E", # pycodestyle (erros) — estilo, baseado na PEP 8
"W", # pycodestyle (avisos) — estilo, baseado na PEP 8
"I", # isort — organização de imports
"B", # flake8-bugbear — padrões que costumam esconder bugs
"S", # flake8-bandit — segurança
"PL", # Pylint — regras diversas de qualidade
"UP", # pyupgrade — modernização de sintaxe
"SIM", # flake8-simplify — simplificação de código redundante
"PT", # flake8-pytest-style — convenções para testes com pytest
"C90", # mccabe — complexidade ciclomática
]
# Regras específicas ignoradas, mesmo dentro de um grupo selecionado
# E501: comprimento de linha — já coberto de outra forma pelo formatador
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
# S101 sinaliza uso de `assert`. Em produção, asserts são removidos
# em modo otimizado (-O), então usar assert para validação real de
# dados é um risco. Em arquivos de teste, porém, assert é a prática
# padrão do pytest — por isso a regra é ignorada só nesse contexto.
"**/tests/*" = ["S101"]
"test_*.py" = ["S101"]
"*_test.py" = ["S101"]
[tool.ruff.format]
quote-style = "single"
Se o projeto não usar pyproject.toml para configuração de ferramentas, o mesmo bloco pode ir num arquivo ruff.toml dedicado, sem o prefixo [tool.ruff].
Vale destacar o per-file-ignores como padrão de boa prática: em vez de desabilitar uma regra globalmente porque ela "atrapalha nos testes", a configuração desliga a regra apenas onde ela genuinamente não se aplica — mantendo a checagem ativa em todo o resto do código de produção.
Adotando o Ruff num projeto legado, sem travar o time
Um cenário comum: o time decide adotar o Ruff, roda ruff check . --statistics, e o relatório mostra milhares de ocorrências acumuladas ao longo de anos de código legado. Tentar corrigir tudo de uma vez, antes de poder sequer ativar o Ruff no pipeline de CI, não é realista — e trava a adoção da ferramenta indefinidamente.
A saída prática é:
ruff check --add-noqa .
Esse comando adiciona um comentário # noqa nas linhas que já apresentam alguma ocorrência — silenciando o aviso especificamente ali. A partir desse ponto, o Ruff pode ser ativado no pipeline de CI sem quebrar o build por causa de código legado: o pipeline passa a exigir qualidade apenas em código novo, enquanto o código antigo continua sinalizado, mas silenciado.
O ponto de atenção aqui é que --add-noqa é um ponto de partida, não uma solução definitiva. Sem um plano deliberado de revisitar essas linhas — seja numa sprint dedicada, seja removendo alguns # noqa a cada PR que já estiver mexendo naquela área do código —, o débito técnico simplesmente fica arquivado sob um comentário, em vez de resolvido. Vale tratar cada # noqa adicionado dessa forma como um item de backlog, não como aprovação silenciosa e permanente da situação atual.
Integrando ao fluxo de trabalho: pre-commit e CI
O maior ganho do Ruff aparece quando ele deixa de ser um comando rodado manualmente e passa a fazer parte do fluxo automático do time. Um padrão comum é usar o pre-commit para rodar o Ruff antes de cada commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
E, no pipeline de CI, uma verificação que falha o build se houver qualquer ocorrência não resolvida (sem o --fix, que é destinado ao ambiente local, não ao CI):
ruff check .
ruff format --check .
O --check no ruff format verifica se o código já está formatado corretamente, sem alterar nada — o comando ideal para uma etapa de CI, que deve detectar problemas, não corrigi-los silenciosamente durante o build.
Boas práticas
- Rode
--statisticsantes de qualquer ação corretiva ao adotar o Ruff num projeto existente — entender o tamanho do problema evita decisões precipitadas. - Use
--fixno dia a dia local, mas nunca no CI — o pipeline deve falhar e sinalizar o problema, não corrigir e commitar silenciosamente por conta própria. - Trate
--add-noqacomo uma ponte, não como destino final — sem revisão programada, o código legado silenciado nunca é de fato corrigido. - Prefira
per-file-ignoresaignoreglobal sempre que a exceção for específica de um contexto (como testes) — mantém a regra ativa em todo o resto do código. - Fixe a versão do Ruff no projeto (via
uv add --dev ruffou pinning explícito norequirements) — evita que o pipeline de CI e o ambiente local de cada dev divirjam de comportamento por rodarem versões diferentes. - Integre com pre-commit, não só com CI — pegar o problema antes do commit é mais barato do que pegar depois, num pipeline que já rodou e falhou.
- Comece com um
selectenxuto (F,E,W,Ijá cobrem boa parte do valor) e vá expandindo o conjunto de regras aos poucos, em vez de habilitar tudo de uma vez num projeto que nunca teve lint — isso evita um primeiro relatório assustador demais para ser acionável.
Conclusão
O ganho do Ruff não é só "é mais rápido" — embora seja, e por uma ordem de grandeza que faz diferença real em pipelines de CI. O ganho maior é a consolidação: uma única ferramenta, uma única configuração, um único ponto de manutenção, cobrindo o que antes exigia manter cinco ferramentas independentes sincronizadas em versão e comportamento. Para quem está começando agora com Python, vale adotar o Ruff desde o primeiro projeto — é mais simples aprender uma ferramenta bem documentada do que cinco ferramentas que precisam conversar entre si.