Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can identical types be incompatible in MyPy?

With the following example:

from typing import Callable, Generic, Type, TypeVar

XType = TypeVar('XType', bound=int)


class C(Generic[XType]):
    def f(self, x_init: XType) -> XType:
        return x_init


def combinator(c_cls: Type[C[XType]]) -> Callable[[C[XType], XType], XType]:
    old_f = c_cls.f

    def new_f(c: C[XType], x_init: XType) -> XType:
        return old_f(c, x_init)

    return new_f

MyPy says:

a.py:15: error: Incompatible return value type (got "XType", expected "XType")
a.py:15: error: Argument 1 has incompatible type "C[XType]"; expected "C[XType]"
a.py:15: error: Argument 2 has incompatible type "XType"; expected "XType"
like image 892
Neil G Avatar asked Apr 19 '20 07:04

Neil G


People also ask

How do I reduce MYPY errors?

You can use # type: ignore[override] to silence the error.

What is MYPY Python?

“Mypy is an optional static type checker for Python that aims to combine the benefits of dynamic (or 'duck') typing and static typing. Mypy combines the expressive power and convenience of Python with a powerful type system and compile-time type checking.” A little background on the Mypy project.

What is TypeVar?

In short, a TypeVar is a variable you can use in type signatures so you can refer to the same unspecified type more than once, while a NewType is used to tell the type checker that some values should be treated as their own type.


Video Answer


2 Answers

I am not sure I agree with the premise of this question.

Here’s part of the docstring from 3.8

class TypeVar(_Final, _Immutable, _root=True):
    """Type variable.
    Usage::
      T = TypeVar('T')  # Can be anything
      A = TypeVar('A', str, bytes)  # Must be str or bytes

    ....
    def __init__(self, name, *constraints, bound=None,
                 covariant=False, contravariant=False):
    ....

Now, if you had just

ThetaType = TypeVar('ThetaType')
XType = TypeVar('XType')

would you be arguing that uses of ThetaType should be considered uses of XType, even though 2 different typevars were setup? Why would adding the bound optional argument automatically collapse them back together? The source does not enforce presence of bound, or any arguments beside name, in any way.

I don't think it’s typing/mypy’s job to infer your intentions in type declarations, only to check your code vs your declared type intentions. If you mean them to be the same then declare only 1 TypeVar. Considering them the same could lose some semantic meaning if you had actual reasons to have 2.

I’ll add to that bound allows more flexibility than constraints as it matches on subclasses. Let’s say you’ve user-defined 4 subclasses of int. Int1(int), Int2, Int3, Int4.... Now you’ve decided to partition your code where some of it should only accept Int1 and Int2. Typevarint12 could somewhat express that, even though your subclasses all match bound=int.

like image 97
JL Peyret Avatar answered Oct 20 '22 00:10

JL Peyret


I support @JL Peyret's point that this is intended behavior.

A couple of additional thoughts:

  1. this isn't specific to TypeVar: if you create two identical classes which have different names, mypy will raise an error if you use them interchangeably

  2. if I understand what you're trying to do, you should be using NewType instead of TypeVar. The point of using TypeVar combined with Generic is to be able to tell mypy (or anyone reading code really) "the same type will go in and out. It might be of type int, float, ...".

Example:

from typing import Generic, TypeVar

T = TypeVar("T", int, str)


def double(value: T) -> T:
    # functional version
    return value * 2


class Doubler(Generic[T]):
    # class version - not sure why someone would do this,
    # it's just an example.
    def __init__(self, value: T):
        # tip: use dataclasses or attrs instead
        self.value = value

    @property
    def double(self) -> T:
        return self.value * 2  # note this works on ints and strings

I think what you're trying to do is actually having types that are integers (and only integers), but you want to be able to track them independently to avoid accidentally switching them around:

from typing import NewType, Tuple

ThetaType = NewType("ThetaType", int)
XType = NewType("XType", int)


def switch_arguments_order(theta: ThetaType, x: XType) -> Tuple[XType, ThetaType]:
   # If you accidentally got the wrong order here, mypy would catch it.
   # You can also think of a function that only returns one of the two,
   # if you accidentally return the wrong one mypy will catch that.
   return x, theta

Now, if you want any piece of logic to handle ThetaType or Xtype without making a distinction, then you can have it return int, and mypy won't complain.


def mypy_passes(x: Xtype) -> int:
    return x


def mypy_fails(n: int) -> Xtype:
    return n


def mypy_passes_with_wrapping(n: int) -> Xtype:
    return Xtype(n)
like image 31
Giorgio Balestrieri Avatar answered Oct 20 '22 01:10

Giorgio Balestrieri