Skip to content

Instantly share code, notes, and snippets.

@nicoddemus
Created February 9, 2021 13:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicoddemus/bfbafd6e11b70db70b6873b6216e4987 to your computer and use it in GitHub Desktop.
Save nicoddemus/bfbafd6e11b70db70b6873b6216e4987 to your computer and use it in GitHub Desktop.

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:

  1. 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.

  1. 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.

  1. 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.

  1. 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. 🙃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment