Com relação à type-hints, eu também sou bastante fã.
Type hints, quando rodados junto com um type checker como o mypy, trazem muitas vantagens:
- Documentação.
Com certeza não substitui a documentação escrita, mas colocar type hints realmente facilita, pois além de não depender das docstrings, ainda temos a ferramenta checando para você, evitando que ela fica desetualizada.
Eu já vi muitas e muitas vezes (e escrevi também) documentação que até estava correta inicialmente, mas depois de um tempo ficou desatualizada. Por exemplo, dizer que um método retorna um dict
de str
-> int
, mas devido à algum refactoring ele às vezes retornava um valor str
(ao invés de int
), quebrando o cliente. Aqui o type checker vai detectar o problema imediatamente.
- Checagem formal dos parâmetros.
O type checker garante que tu aceita/retorna o que tu pensa que aceita/retorna. Exemplo de código em produção:
def get_current_caption(options):
"""
Returns the caption of the current selected option.
:type Options option:
:rtype: str
"""
for option, caption in options.captions():
if option == options.current:
return caption
(A classe Options
não importa muito para o exemplo).
O código acima tinha testes, e tinha 100% de coverage também, mas está com um bug, botando type checking:
def get_current_caption(options: Options) -> str:
"""
Returns the caption of the current selected option.
"""
for option, caption in options.captions():
if option == options.current:
return caption
Aqui o mypy
na hora detecta o problema: existe um return None implicito ali. Como comentei, mesmo 100% de coverage não pega esse tipo de problema, pois temos aqui uma checagem formal. Agora até te ajuda a decidir: quero mesmo retornar None
, ou jogar uma exceção?
Notem também que dá pra deixar a docstring mais concisa, pois os tipos já estão na assinatura.
- Facilita o refactoring.
Relacionada com a de cima, o type checker vai te ajudar muito em refactorings. Por exemplo, mudar um método que sempre retornava uma instância de uma classe, passar a retornar também None às vezes: o type-checker vai te apontar todos os lugares do código que agora tu tem que verificar None. Aqui em especial os testes provavelmente não iriam te ajudar, mesmo com 100% de cobertura, pois nunca foi considerado que poderia retornar None. Vai ser adicionados mais testes aqui, procurar todos os callers, etc, mas o type checker é uma rede de segurança à mais aqui (e até mais confiável).
Outro exemplo: uma função tipo get_names()
que hoje retorna uma lista de str. Mas alguém decide refatorar isso para retornar um set
de str
. A chance de não quebrar alguém depende do uso (se alguém só usar for no resultado da função tudo continua funcionando por exemplo), mas se alguém assume que o retorno é uma lista (por exemplo chama .sort()
), o mypy
vai apontar o problema na hora, antes de rodar os testes.
- Torna APIs mais claras e resilientes.
Se tu mantém uma biblioteca usada por outros, os type hints são um ótimo jeito de garantir que os usuários estão usando a API corretamente, e mais importante, estão usando do jeito que você quer que eles usem.
Voltando o exemplo do get_names()
acima, tu pode documentar "Volta uma sequencia de strings, não assuma que volta nenhum tipo especificio", mas a documentação não vai impedir alguém de chamar .sort()
se tua implementação hoje retorna um list
.
Com type hints, tu pode declarar que retorna Sequence[str]
e os clientes não vão conseguir usar como uma lista, facilitando tu refatorar isso no futuro.
Mais de uma vez eu tive que alterar um tipo de retorno assim, em que a documentação falava uma coisa mas que no final a documentação é só um "por favor use como uma sequencia", mas não existe mesmo nenhuma garantia nesse sentido.
Se no futuro tu decidir explicitamente quebrar a API, a mudança de assinatura vai ser um jeito claro de comunicar isso, inclusive pode ser automatizado: imagine que você salve as assinaturas da sua API em um arquivo, e automaticamente detecte que uma assinatura mudou de forma incompatível (adicionou um novo parâmetro não default, mudou o tipo de um parâmetro ou de retorno, etc), e isso seja um sinal que tem que mudar o major version da tua API (se tu usa semantic versioning por exemplo). Ter os tipos como um metadado diretamente acessível no objetivo é muito valioso, além de permitir esse tipo de automatização.
a) Claro, para scripts pequenos é pouco ganho, mas para códigos já um pouco maiores acredito que tenha muitos benefícios (e não quero dizer também milhões de linhas de código não, acho que mais que 2 desenvolvedores já vale a pena).
b) Com relação à poluir o código/ser feio, acredito que é só questão de acostumar, além do que no Python 3.9+ tu já usa até os tipos builtins de containers, por exemplo list[str]
ao invés de ter que usar typing.List[str]
. Aqui vale lembrar um pouco de história: até o with
já foi considerado "feio" quando foi introduzido, mas hoje todo mundo gosta. Mesma coisa com list/generator comprehensions, muita gente torceu o nariz quando viu pela primeira vez. Recentemente tem também o operador :=
, minha primeira impressão foi não gostar, mas quando comecei a usar realmente vi que deixa o código mais simples e sucinto.
Enfim, como dá pra ver estou cada vez mais fã de type hints. 🙃