Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type annotate a function parameter as derived from multiple abstract base classes in Python

Tags:

python

mypy

I'm trying to type annotate a function in Python 2 according to PEP 484. The function accepts a container which should implement both __len__ and __iter__. The original code where I want to add this annotation is quite complex, so consider an example function which returns the product of all ints in a container s if len(s) is even and returns 1 otherwise.

If I wanted to annotate a container where only __len__ is needed, I would have annotated it as type: (Sized) -> int. If I wanted to annotate a container where only __iter__ is needed, I would have annotated it as type: (Iterable[int]) -> int. But how do I perfectly annotate a container where I need both?

I tried this as per Piotr-Ćwiek's suggestion:

from __future__ import print_function
from typing import Sized, Iterable

class SizedIterable(Sized, Iterable[int]):
    pass

def product2(numbers):
    # type: (SizedIterable) -> int
    if len(numbers)%2 == 1:
        return 1
    else:
        p = 1
        for n in numbers:
            p*= n
        return p

print(product2([1, 2, 3, 4]))
print(product2({1, 2, 3, 4}))

but this failed with this error:

prod2.py:17: error: Argument 1 to "product2" has incompatible type List[int]; expected "SizedIterable"
prod2.py:18: error: Argument 1 to "product2" has incompatible type Set[int]; expected "SizedIterable"
like image 976
Eklavya Sharma Avatar asked May 19 '16 16:05

Eklavya Sharma


People also ask

What is __ annotations __ in Python?

Annotations were introduced in Python 3.0 originally without any specific purpose. They were simply a way to associate arbitrary expressions to function arguments and return values. Years later, PEP 484 defined how to add type hints to your Python code, based off work that Jukka Lehtosalo had done on his Ph. D.

How do you annotate type in Python?

To annotate return value type, add -> immediately after closing the parameter parentheses, just before the function definition colon( : ): def announcement(language: str, version: float) -> str: ... The function now has type hints showing that it receives str and float arguments, and returns str .

What are function annotations in Python?

What are Function annotations? Function annotations are arbitrary python expressions that are associated with various part of functions. These expressions are evaluated at compile time and have no life in python's runtime environment. Python does not attach any meaning to these annotations.

What is a type annotation?

What Are Type Annotations? Type annotations — also known as type signatures — are used to indicate the datatypes of variables and input/outputs of functions and methods. In many languages, datatypes are explicitly stated. In these languages, if you don't declare your datatype — the code will not run.


1 Answers

In python 3.6, there's typing.Collection that works almost perfectly for your use case (it also derives from Container, but realistically anything that you want to use will likely have __contains__). Unfortunately, there's no solution for python 2.

The reason SizedIterable doesn't work is that by deriving it from Sized and Iterable, you are only telling mypy that it's a subtype of those two types; mypy does not conclude that any type that's a subtype of Sized and Iterable is also a subtype of SizedIterable.

Mypy's is perfectly logical; after all, you wouldn't want this code to type check:

class A(Sized, Iterable[int]):
  def g(self) -> None:
    ...

def f(x: A) -> None:
  a.g()

# passes type check because [1, 2] is Sized and Iterable
# but fails in run-time
f([1, 2])

It would be too tricky if mypy treated your class definition differently just because its class body is empty.

In order for mypy to understand your intent, mypy would need to add a new feature to its type system.

Two options for such a feature are being considered at the moment:

  • intersections (as pointed out by @PiotrĆwiek); what you're asking for is an intersection of Iterable and Sized
  • structural typing; this is much simpler than general intersections, but sufficient for your use case, since all you need is for the numbers argument to have __len__ and __iter__ methods

Mimicking typing class definitions or using __instancecheck__ won't work because (as someone explained to me just recently) under no condition will mypy ever run the code you wrote (i.e., it will never import your module, never call your function, etc.). This is because mypy is a static analysis tool that doesn't assume that the environment to run your code is even available during its execution (e.g., python version, libraries, etc.).

like image 150
max Avatar answered Oct 26 '22 06:10

max