13

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"
Neil G
  • 32,138
  • 39
  • 156
  • 257
  • It looks like mypy interprets `new_find_fixed_point` as a generic function with its own separate instantiation of `ThetaType` and `XType`. – user2357112 Apr 19 '20 at 07:24
  • @user2357112supportsMonica Any idea how I could fix it? – Neil G Apr 19 '20 at 07:25
  • I suspect you can't; mypy itself would have to change. – user2357112 Apr 19 '20 at 07:25
  • shot in the dark, but what happens if you add `@classmethod`? – Purag Apr 19 '20 at 07:28
  • (You could probably work around it - I suspect introducing an extra class would help - but I doubt there's any way to just change the type hints to get mypy to understand what you're going for.) – user2357112 Apr 19 '20 at 07:28
  • @user2357112supportsMonica Is this a bug with mypy? Should I report it? I'm still new at type annotations. – Neil G Apr 19 '20 at 07:30
  • 1
    Take a look at https://github.com/python/mypy/issues/708, seems like a known issue that isn't a priority. Confirm if it's related please – arshbot Apr 19 '20 at 07:31
  • 1
    @HarshaGoli: That looks similar at first glance, but it seems to be a completely different issue resulting from how methods are handled. – user2357112 Apr 19 '20 at 07:32
  • 1
    @NeilG: I would personally consider it either a bug or a deficiency in mypy. – user2357112 Apr 19 '20 at 07:35
  • 1
    @user2357112supportsMonica Thanks for your help. I've reported this issue and look forward to learning from the experts https://github.com/python/mypy/issues/8696 – Neil G Apr 19 '20 at 07:54

2 Answers2

1

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.

JL Peyret
  • 10,917
  • 2
  • 54
  • 73
  • Yes, thé two types are different. I don't think it matters for this question though. – Neil G Apr 19 '20 at 18:45
  • 3
    @NeilG then perhaps you should modify your question so that the title and your comments concerning your particular code that shows this unexpected behavior indicate why you are expecting something different. Right now, I see nothing in the TypeVar building block itself indicating why 2 typevars should behave the same. If the way you set up your code around TypeVar should lead to equivalence, then please explain that, but don't *pin it just on TypeVar*. – JL Peyret Apr 19 '20 at 19:42
  • I never said that the two different typevars should behave the same. The errors shows each typevar being incompatible with itself. It's true that I could maybe have found a smaller MWE, but I didn't want to invest more time into this. – Neil G Apr 19 '20 at 21:19
  • To be more specific, if you set up two different `TypeVar`s, it's because you don't want them to be interchangeable, otherwise you'd just use one. – Giorgio Balestrieri Dec 02 '21 at 12:27
  • @GiorgioBalestrieri I've simplified the MWE now to clarify the problem. – Neil G Dec 02 '21 at 21:37
1

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)
  • I don't think you've understood the problem? Did you look at the issue I filed? The problem is that the generic type of an inner function cannot be made to match the generic type of an enclosing function. – Neil G Dec 02 '21 at 14:08