Tipo assim... tipo em Python

Tipos em Python é aquele "tipo" de assunto que vai agitar qualquer papo de almoço entre programadores. Eu mesmo fui cético por muito tempo, e franzia a testa constantemente quando esse tópico aparecia. Mas já mudei de opinião algumas vezes, e escrever sobre tais mudanças rendeu alguns dos artigos mais acessados do blog. De IDEs vs. editores, passando por GraphQL e chegando até mesmo ao uso de Java. Agora é a vez de tipagem em Python.

Nota-se uma "nem tão recente onda" na utilização de tipagem estática. Alimentada por linguagens como Go e Typescript, os argumentos (assim como prós e contras) são dos mais variados, e vem influenciando linguagens amplamente conhecidas por serem de tipagem dinâmica, como Python e Ruby.

Mais fundamental que entender o "como", é crucial ficar por dentro da motivação por trás da PEP 484. Vamos viajar um pouquinho no tempo e tentar compreender o "por quê".

No meu tempo é que era bom

Conheci o Python lá pelos idos de 2009, e venho falando sobre a linguagem desde então. Sempre posso contar com o artigo do Wikipedia para definir a linguagem com os atributos que mais admiro:

Python é uma linguagem de programação de alto nível, interpretada, de script, imperativa, orientada a objetos, funcional, de tipagem dinâmica e forte.

E sem mais rodeios, abrimos o nosso primeiro parênteses.

Tipagem dinâmica e forte

Python, PHP, Javascript e Ruby tem algo em comum: Elas são linguagens de tipagem dinâmica. Em outras palavras, elas são capazes de escolher o tipo de dado de acordo com o valor que está sendo atribuído a uma variável:

a = "foo"

a = 1
b = 2
print(a + b)  # 3

Ao contrário de linguagens com tipagem estática, como o Java; onde uma vez que a variável esteja setada com o seu respectivo tipo, não podemos mais alterá-lo:

public class MyClass {
    public static void main(String args[]) {
        String a = "foo";
        a = 1;  // TypeError
    }
}

Mesmo se utilizarmos recursos mais modernos, como a inferência de tipo, ainda assim estamos sujeitos à tipagem estática:

public class MyClass {
    public static void main(String args[]) {
        var a = "foo";
        a = 1;  // TypeError
    }
}

O Javascript é um exemplo de linguagem com tipagem fraca. Com ela somos capazes de misturar tipos sem maiores restrições, e contar com conversões realizadas automaticamente pela linguagem, como no exemplo abaixo:

var a = "foo";
var b = 2;

console.log(a + b); // foo2

O mesmo não acontece com o Python, que tem por característica a tipagem forte, sendo assim mais restritivo com esse tipo de operação:

a = "foo"
b = 2

print(a + b)  # TypeError: can only concatenate str (not "int") to str

Tipagem dinâmica e forte foram duas das características mais apaixonantes do Python. A linguagem estava me ajudando a ser mais coerente com o design do código, e com isso, me dando uma camada a mais de segurança (e por consequência, qualidade).

Type hints

A primeira vez que ouvi falar sobre tipagem estática em Python, foi por volta de 2016. Segundo o "Our journey to type checking 4 million lines of Python", no ano anterior (com o Python 3.5) era lançada a PEP 484.

Essa Python Enhancement Proposal oficializava a proposta de Jukka Lehtosalo para o uso de tipos em Python. A diferença aqui, em relação a outras linguagens, é que utilizamos anotações para sugerir qual tipo uma variável ou parâmetro receberá (chamadas de type hints):

def greeting(name: str) -> str:
    return 'Hello ' + name

Por anotações entende-se que estamos sugerindo ao programador, e não ao intepretador, quais são os tipos. De fato, o interpretador ignora tais instruções e elas não tem efeito no código rodando.

Na época, o mypy não era essa lib bem estabelecida que é hoje. Conceitos e ferramentas ao redor desse tópico ainda eram confusos, e o mesmo gerava debates acalorados.

Nem preciso dizer que fui um dos programadores que logo de cara torceu o nariz.

Static type é cool novamente

Quantas vezes você já precisou abrir uma função para compreender o que exatamente ela retornava? Quantos Docstrings você leu, dando dicas de tipos de parâmetros e retorno? Quantos print e pdb.set_trace() para entender a razão de um atributo de instância estar disparando algum erro de tipo ou chave?

À medida que o seu projeto vai crescendo em tamanho e complexidade, a ausência de tipos começa a afetar a qualidade do mesmo, pelo menos é isso que afirma Dustin Ingram no talk Static Typing in Python:

Gráfico com a complexidade subindo ao aumentar linhas de código
Quando migrar para Type Hint (youtube.com)

Se você tiver 28 minutos, dê uma pausa neste artigo e confira a apresentação na íntegra:

Há o argumento de que se o código fosse claro, seguindo padrões e boas práticas, não precisaríamos de tais artifícios. Acho difícil discordar dessa afirmação. Tão difícil quanto encontrar tal código.

Código legível é subjetivo, enquanto que tipagem estática serve como uma forma de documentação e (supostamente) auxilia na manutenibilidade de software. Esse é um dos argumentos apresentados no paper "An empirical study on the impact of static typing on software maintainability".

Em suma, com o passar do tempo, fazendo parte de grandes projetos front-end em React, e utilizando de tipagem para entender o que estava acontecendo, eu enxergo e entendo o argumento a favor do uso de type hints em Python.

Como faz?

A melhor forma de começar é através do mypy:

$ pip install mypy

O próximo passo é adicionar as anotações ao código, indicando os tipos:

# calculadora.py

def soma(a: int, b: int) -> int:
    return a + b

result = soma(2.5, 1)
print(result)

Anotamos os parâmetros da função através de : int, e o retorno com -> int. Com o utilitário de linha de comando executamos a checagem:

$ mypy calculadora.py
Success: no issues found in 1 source file

Se alterarmos um dos parâmetros passados para float, o mypy nos alertará que há uma inconsistência:

$ mypy calculadora.py
calculadora.py:5: error: Argument 1 to "soma" has incompatible type "float"; expected "int"
Found 1 error in 1 file (checked 1 source file)

Mas ainda assim será possível executar o código sem problemas.

VS Code

Com o VS Code e o plugin Python instalado, basta o interpretador configurado ter acesso ao mypy, ou que você explicitamente passe o caminho do executável nas configurações do editor:

Interface do VS Code mostrando um erro de tipo
Como o erro do mypy é exibido no VS Code

Leia mais sobre como configurar o Mypy com VS Code.

Vim

No Vim há diferentes alternativas. Talvez a forma mais simples e prática seja com o uso do vim-ale.

Caso você se aventure pelo mundo do LSP, uma combinação interessante é a do vim-lsp + Python Language Server:

$ pip install pip install python-lsp-server pylsp-mypy

Interface do Vim mostrando um erro de tipo
Como o erro do mypy é exibido no vim com vim-lsp

Considerações finais

Volto a mencionar a minha experiência em um projeto complexo em Javascript: Ter os tipos declarados e explícitos foi de muita ajuda, e sem dúvida acelerou o desenvolvimento e o onboarding de novos membros à equipe com o passar do tempo. Não é tão difícil de enxergar as vantagens por esses termos.

Mas o debate sobre Python em si ser um bom candidato a esse tipo de prática é muito válido. Fazendo um contraponto ao parágrafo acima, um código bem escrito poderia sim ser o suficiente para resolver alguns dos problemas que estamos tentando resolver com type hint.

Eu ainda trocaria um código mal escrito por um bem escrito, ao invés de um mal escrito por um mal escrito com type hints.

Em cima do muro :)

Quem sabe no próximo grande projeto...

Até a próxima.

Referência