
Acho que estamos nos acostumando lentamente ao fato de o Python ter anotações de tipo: elas foram trazidas de volta dois releases (3.5) nas anotações de funções e métodos ( PEP 484 ) e, no último release (3.6), às variáveis ( PEP 526 ).
Como os dois PEPs foram inspirados pelo MyPy , vou lhe dizer quais alegrias mundanas e dissonâncias cognitivas me aguardavam ao usar este analisador estático, bem como o sistema de digitação como um todo.
Disclamer: Não levanto a questão da necessidade ou nocividade da digitação estática em Python. Estou apenas falando das armadilhas que me deparei enquanto trabalhava em um contexto estaticamente digitado.
Genéricos (digitando.Generic)
É bom usar algo como List[int] , Callable[[int, str], None] nas anotações.
É muito bom quando o analisador destaca o seguinte código:
 T = ty.TypeVar('T') class A(ty.Generic[T]): value: T A[int]().value = 'str'  
No entanto, e se escrevermos uma biblioteca e o programador que a usar não usará um analisador estático?
Forçando o usuário a inicializar a classe com um valor e, em seguida, armazene seu tipo?
 T = ty.TypeVar('T') class Gen(Generic[T]): value: T ref: Type[T] def __init__(self, value: T) -> None: self.value = value self.ref = type(value) 
De alguma forma, não é fácil de usar.
Mas e se você quiser fazer isso?
 b = Gen[A](B()) 
Em busca de uma resposta para essa pergunta, repassei um pouco a typing e mergulhei no mundo das fábricas.

O fato é que, após inicializar a instância da classe Generic, ela possui o atributo __origin_class__ , que possui o atributo __args__ , que é uma tupla de tipo. No entanto, o acesso a partir de __init__ , bem como de __new__ , não é. Também não está na metaclasse __call__ . E o truque é que, no momento da inicialização da subclasse de Generic ele se transforma em outra metaclasse _GenericAlias , que define o tipo final, depois que o objeto é inicializado, incluindo todos os métodos de sua metaclasse, ou no momento em que __getithem__ . Portanto, não há como obter tipos genéricos ao construir um objeto.
Jogamos esse lixo, prometeu uma solução mais universal.Portanto, escrevi para mim um pequeno descritor que resolve esse problema:
 def _init_obj_ref(obj: 'Gen[T]') -> None: """Set object ref attribute if not one to initialized arg.""" if not hasattr(obj, 'ref'): obj.ref = obj.__orig_class__.__args__[0]  
Obviamente, em conseqüência, será necessário reescrever para uso mais universal, mas a essência é clara.
 [UPD]: De manhã, decidi tentar fazer o mesmo que no próprio módulo de typing , mas mais simples:
 import typing as ty T = ty.TypeVar('T') class A(ty.Generic[T]):  
[UPD]: O desenvolvedor de typing Ivan Levinsky disse que as duas opções podem quebrar imprevisivelmente.
De qualquer forma, você pode usar de qualquer maneira. Talvez __class_getitem__ seja ainda um pouco melhor, pelo menos __class_getitem__ é um método especial documentado (embora seu comportamento para genéricos não seja).
Funções e Aliases
Sim, os genéricos não são nada fáceis:
Por exemplo, se em algum lugar aceitarmos uma função como argumento, sua anotação passa automaticamente de covariante para contravariante:
 class A: pass class B(A): pass def foo(arg: 'A') -> None:  
E, em princípio, não tenho queixas sobre lógica, apenas isso deve ser resolvido através de aliases genéricos:
 TA = TypeVar('TA', bound='A') def foo(arg: 'B') -> None:  
Em geral, a seção sobre variabilidade de tipos deve ser lida com cuidado e mais de uma vez.
Compatibilidade com versões anteriores
Isso não é tão bom: da versão 3.7 Generic é uma subclasse do ABCMeta , que é muito conveniente e boa. É ruim que isso quebre o código se estiver sendo executado no 3.6.
Herança Estrutural (Suputagem Estrutural)
No começo fiquei muito feliz: as interfaces foram entregues! O papel das interfaces é desempenhado pela classe Protocol do módulo typing_extensions , que, em combinação com o decorador @runtime , permite verificar se a classe implementa a interface sem herança direta. O MyPy também é destacado em um nível mais profundo.
No entanto, não notei muitos benefícios práticos no tempo de execução em comparação à herança múltipla.
Parece que o decorador verifica apenas a presença de um método com o nome necessário, sem mesmo verificar o número de argumentos, sem mencionar a digitação:
 import typing as ty import typing_extensions as te @te.runtime class IntStackP(te.Protocol): _list: ty.List[int] def push(self, val: int) -> None: ... class IntStack: def __init__(self) -> None: self._list: ty.List[int] = list() def push(self, val: int) -> None: if not isinstance(val, int): raise TypeError('wrong pushued val type') self._list.append(val) class StrStack: def __init__(self) -> None: self._list: ty.List[str] = list() def push(self, val: str, weather: ty.Any=None) -> None: if not isinstance(val, str): raise TypeError('wrong pushued val type') self._list.append(val) def push_func(stack: IntStackP, value: int): if not isinstance(stack, IntStackP): raise TypeError('is not IntStackP') stack.push(value) a = IntStack() b = StrStack() c: ty.List[int] = list() push_func(a, 1) push_func(b, 1)  
Por outro lado, o MyPy, por sua vez, se comporta de maneira mais inteligente e destaca a incompatibilidade dos tipos:
 push_func(a, 1) push_func(b, 1)  
Sobrecarga do operador
Um tópico muito recente, porque ao sobrecarregar os operadores com toda a segurança do tipo, toda a diversão desaparece. Essa pergunta apareceu várias vezes no rastreador de erros do MyPy, mas ainda é difícil em alguns lugares, e você pode desativá-lo com segurança.
Eu explico a situação:
 class A: def __add__(self, other) -> int: return 3 def __iadd__(self, other) -> 'A': if isinstance(other, int): return NotImplemented return A() var = A() var += 3  
Se o método de atribuição composto retornar NotImplemented , o Python procurará primeiro __radd__ , depois usará __add__ e voila.
O mesmo se aplica à sobrecarga de qualquer método de subclasse do formulário:
 class A: def __add__(self, x : 'A') -> 'A': ... class B(A): @overload def __add__(self, x : 'A') -> 'A': ... @overload def __add__(self, x : 'B') -> 'B' : ... 
Em alguns lugares, os avisos já foram movidos para a documentação, em alguns locais eles ainda trabalham no produto. Mas a conclusão geral dos colaboradores é deixar essas sobrecargas aceitáveis.