Começando do zero com Pylint

Publicado em 2018-07-11 por Vinicius Assef

Esse artigo conta sobre meus primeiros contatos com um analisador de código, as lições que eu aprendi e algumas dicas práticas sobre como inserir o Pylint no seu projeto já em andamento.

Pylint é um analisador estático de código Python. Sendo assim, ele não executa seu programa; "apenas" analisa características do código fonte e aponta possíveis melhorias. Dentre os muitos aspectos que o Pylint verifica, alguns são mais corriqueiros, tais como:

  • Linhas muito longas;

  • Variáveis usadas sem ter sido criadas antes;

  • Módulos importados que não foram usados;

  • Docstrings faltando, etc.

Outros, podem ser menos óbvios, como apontar que uma classe tem muitos atributos ou que um nome de variável está fora do padrão.

Além disso, o Pylint pode ajudar a melhorar algumas construções. Por exemplo, através das checagens de refactoring ele sugere trocar:

if param.lower() in ["sim", "1", "yes"]:
   opcao = True
else:
   opcao = False

por

opcao = bool(param.lower() in ["sim", "1", "yes"])

Primeiro contato

Toda essa capacidade do Pylint me animou e eu resolvi usá-lo em um projeto já iniciado.

Comecei lendo o tutorial oficial. Ele é bem resumido e ajuda a começar rápido. Nele, eu aprendi três coisas que mostraram-se muito úteis:

  1. Como executá-lo;

  2. Ver a descrição de uma mensagem para entender o que ela realmente significa;

  3. Desabilitar uma checagem. Por exemplo, não avisar que existem linhas muito longas no programa.

Sabendo, agora, como ele funciona, fui instalá-lo no meu virtualenv ativo (em Python 3):

(venv) $ python -m pip install pylint

Importante: o Pylint precisa ser instalado no virtualenv de cada projeto porque algumas checagens que ele faz variam de acordo com a versão de Python que vai executar seu programa.

Quando fui rodar o Pylint no meu projeto, percebi que não devemos informar a ele o nome de um arquivo .py. Ao invés disso, ele espera o nome de um módulo ou de um package. Na prática, você precisa informar o nome de um diretório (package) para analisar os módulos dele. Se quiser ser mais específico, informe um módulo, no formato package/modulo, para analisar somente ele.

Eu escolhi o projeto e passei o Pylint em um package:

(venv) $ pylint apps

O susto

O resultado foi desanimador: ele mostrou muitas mensagens. Foram tantas que quase desisti porque não sabia por onde começar a resolver os problemas apontados.

Depois do impacto inicial, pensei melhor, li mais um pouco e descobri que o projeto foi lançado em 2003 (14 anos de idade quando escrevi esse artigo) e continua sendo ativamente desenvolvido. Isso me animou e decidi continuar, prestando mais atenção às mensagens que o Pylint mostrava, tentando encontrar algum padrão.

Uma das mensagens que mais apareciam indicava a ausência de docstrings: missing-docstring. Várias delas diziam que faltavam docstrings em módulos. Outras tantas diziam que as classes não tinham docstrings. E muitas outras diziam que as funções deveriam ter docstrings, mas também não tinham.

Escolhendo o que ver

Diante dessa quantidade enorme de mensagens eu pude usar umas das coisas úteis que aprendi no tutorial: desabilitar uma checagem. Claro, eu não queria ver as mensagens sobre docstrings faltando, já que não contribuíam muito naquele momento. Eu queria ver o Pylint me ajudando em algo mais concreto:

(venv) $ pylint --disable=missing-docstring apps

Ainda apareceram muitas mensagens, mas bem menos do que antes. Uma delas dizia que um módulo havia sido importado, mas não estava sendo usado (unused-import). Essa era uma mensagem útil para mim naquela ocasião, então resolvi ver se esse problema ocorria em outros lugares. Mas como ver apenas esse tipo de mensagem, já que eu queria focar a atenção nela? Bem, eu desabilitei todas as checagens (--disable=all) e habilitei apenas a que eu queria:

(venv) $ pylint --disable=all --enable=unused-import apps

Agora sim, as mensagens eram poucas e eu poderia dar atenção a um aspecto útil do projeto: eliminar os imports não usados.

Resolvi esse problema nos arquivos listados e rodei o Pylint novamente. Agora não mostrava mais nenhuma mensagem de imports não usados.

Mandei o Pylint analisar todo o package outra vez, desabilitando, novamente, apenas a checagem das docstrings:

(venv) $ pylint --disable=missing-docstring apps

As mensagens ainda eram muitas.

Entendendo melhor as mensagens

Prestando um pouco mais de atenção, percebi que cada mensagem é identificada por uma letra, a categoria da mensagem, conforme explicado no tutorial:

  • C (Convention): para violação de padrões de código. Por exemplo, docstrings faltando ou linha muito extensa. O default do Pylint é de 100 caracteres por linha, ao invés de 80, conforme a PEP8 recomenda.

  • E (Errors): para problemas importantes no programa. Exemplo: quando você usa uma variável que não foi criada.

  • R (Refactor): para violações de algumas métricas. Exemplo: muitos métodos públicos em uma classe.

  • W (Warning): para questões de estilo ou problemas menores. Exemplo: argumento de função não usado.

O poder dos Baby Steps

Sabendo que existem categorias de mensagens, que cada uma delas tem um grau de importância diferente e que é possível habilitar ou desabilitar checagens, eu posso escolher qual verificação vou priorizar no meu projeto.

Se o projeto é grande e eu não posso refatorá-lo agora, posso desabilitar as mensagens "R":

(venv) $ pylint --disable=R apps

O importante (e muito útil) é que o Pylint deixa você escolher o que analisar. Fica muito mais fácil começar se você configurá-lo para fazer menos verificações. Como cada projeto tem suas próprias características, é preciso decidir caso a caso. Eu sugiro adotar a seguinte abordagem:

  1. Conhecer que tipos de mensagens o Pylint mostra para o seu projeto da forma padrão, sem nenhuma configuração especial;

  2. Escolher o que analisar agora (--enable) e o que deixar para depois (--disable);

  3. Resolver os problemas que você julgar importantes para o momento;

  4. Passar para o próximo nível de qualidade, resolvendo problemas menos sérios, mas também importantes.

Começando pra valer

Eu comecei habilitando apenas as mensagens ERROR (--disable=all --enable=E) e ignorando todas as outras categorias. Resolvi todos os problemas relatados. Na maioria das vezes isso consertou problemas nos meus programas e mostrou que meus testes (automatizados ou manuais) precisavam melhorar.

O próximo passo foi habilitar os WARNINGS. Eles me ajudaram a "limpar" os programas. Módulos que eram importados mas não eram usados e variáveis criadas mas não usadas foram os campeões de problemas. Provavelmente eles ficaram sobrando porque alguma manutenção apagou o código que os usava, mas não apagou as linhas do import ou da criação das variáveis.

As CONVENTIONS foram as próximas. Essa categoria pode mostrar muitas mensagens, principalmente se você não descreve seus objetos (módulos, funções, classes) com docstrings. Para esses casos eu decidi dizer explicitamente ao Pylint que ele deveria ignorar essa checagem para o objeto em questão. Vou explicar como e porquê fazer isso.

Se havia uma mensagem relatando que faltava docstring em determinada função e eu não podia escrevê-la imediatamente, eu incluía um comando para o Pylint ignorar essa checagem apenas para aquela função:

def my_func():
    # pylint: disable=missing-docstring
    # TODO documentar
    ...

Fazendo isso eu deixei explícito onde o Pylint não deveria analisar determinado aspecto do código. Fazer isso para muitos casos dá trabalho. Mas a recompensa é que eu criei a chance de voltar depois naquele programa para documentar o que fosse necessário. Para isso eu adicionei também um comentário # TODO documentar. Para as funções cujo nome era muito significativo e não havia necessidade de documentar nada, eu não incluí o lembrete; deixei só o comando para o Pylint.

Os benefícios de indicar caso-a-caso que análise não deve ser feita, são:

  • Você continua recebendo as mensagens para os casos que precisam ser resolvidos;

  • Você deixa claro onde está convivendo com determinada característica não recomendada no código fonte (explícito é melhor do que implícito);

  • À medida que você vai indicando onde o Pylint não deve analisar, vai recebendo menos mensagens de "falsos positivos";

  • Passa a existir um protocolo de "retorno futuro" (# TODO ...), para consertar o que vale a pena.

Análises mais profundas

Depois das conventions, chegou a vez das REFACTORINGS. Esse é um recurso muito poderoso do Pylint, que consegue identificar trechos duplicados em programas diferentes e sugere melhorias no código fonte, conforme eu exemplifiquei no início desse artigo. No grupo de refactoring também há casos que não vale o esforço de resolver tudo de uma vez. Por exemplo, o Pylint avisa quando uma classe tem muitos atributos e muitos métodos públicos. Refatorar esses casos pode exigir um esforço grande de arquitetura. Para livrar-se dessas mensagens, existem as seguintes alternativas:

  1. Marcar esses casos para não serem checados, como fizemos com as conventions;

  2. Configurar o Pylint para não checar mais essas mensagens específicas, sem mexer no seu código fonte;

  3. Configurar o Pylint para considerar uma quantidade diferente de métodos públicos e atributos.

Cada projeto é um caso diferente e você deve adequar esse comportamento a cada situação.

Vale explicar por que eu resolvi as CONVENTIONS antes das REFACTORING. Como as conventions mostram em que pontos seus programas estão desviando-se dos padrões recomendados, pode ser que alguns programas obedeçam determinadas regras e outros, não. Isso pode acabar confundindo a verificação de refactoring. Por isso eu decidi organizar todo o meu código fonte e só depois partir para melhorá-lo.

Conclusão

Por mais intimidante que seja no início, eu recomendo usar o Pylint. Não posso negar que o começo é tortuoso, mas fica bem mais simples quando você escolhe quais problemas resolver primeiro.

Como regra geral, sugiro essa sequência para um projeto que já existe e você quer começar analisá-lo com Pylint:

  1. Ative apenas a checagem ERROR e resolva o que for listado;

  2. Tire sujeira do seu código resolvendo as WARNINGS (sem desativar a checagem de ERROR);

  3. Habilite a verificação de CONVENTIONS (mantendo as duas anteriores ativadas também) e ataque cada alerta, deixando explícitos os casos que você não vai resolver agora;

  4. Sem desativar nenhuma das anteriores, ative a verificação de REFACTORING para melhorar o aspecto e arquitetura do seu código.

  5. Configure alguns parâmetros do Pylint para considerar as situações específicas do seu projeto.

Use o Pylint para melhorar a qualidade do seu código Python e simplificar as manutenções futuras.

Vinicius Assef

Eu sou apaixonado por Python e shell script.

Aprenda com seus erros e dê nome certo às coisas.