4

I'm trying to implement this answer for custom deepcopy, but with type hints, and mypy's not happy with the Any type that I'm using from a third party library. Here's the smallest bit of code I can get to fail

# I'm actually using tensorflow.Module, not Any,
# but afaik that's the same thing. See context below
T = TypeVar("T", bound=Any)

def foo(x: T) -> None:
    cls = type(x)
    cls.__new__(cls)

I see

 error: No overload variant of "__new__" of "type" matches argument type "Type[Any]"
 note: Possible overload variants:
 note:     def __new__(cls, cls: Type[type], o: object) -> type
 note:     def __new__(cls, cls: Type[type], name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> type

It passes if I bound T to something typed, like int, str or a custom class. I'm confused about this, cos neither of these overloads matches the __new__ docs. My knowledge of __new__ is fairly basic.

I'm after either a fix, or if it's a limitation/bug in mypy, an explanation of what that is.

Context

The actual function is

import tensorflow as tf

T = TypeVar("T", bound=tf.Module)  # tf.Module is untyped

def my_copy(x: T, memo: Dict[int, object]) -> T:
    do_something_with_a_tf_module(x)

    cls = type(x)
    new = cls.__new__(cls)
    memo[id(self)] = new

    for name, value in x.__dict__.items():
        setattr(new, name, copy.deepcopy(value, memo))

    return new 

curiously, If I instead make this a method

class Mixin(tf.Module):
    def __deepcopy__(self: T, memo: Dict[int, object]) -> T:
        ...  # the same implementation as `my_copy` above

there's no error

joel
  • 6,359
  • 2
  • 30
  • 55
  • The signatures are those of calling ``type``. – MisterMiyagi Feb 09 '21 at 14:43
  • @MisterMiyagi sorry i don't understand, can you elaborate? – joel Feb 09 '21 at 14:51
  • `mypy` doesn't know that you assigned a value of type `int` to `x`, only that `x` could be *any* type. As such, it doesn't know what type of either `cls` or `cls.__new__` , and thus it can't tell if `cls` is a valid argument for `cls.__new__`. – chepner Feb 09 '21 at 16:25
  • @chepner that suggests to me that it's not reliable to call `cls.__new__(cls)` on any old result of `type(...)`, because the signature of `__new__` depends on the class. Is that right? – joel Feb 09 '21 at 16:42
  • I'm not sure I would characterize the code as unreliable, rather just something that inherently relies on dynamic typing. Not all valid Python code can be statically typed. Note that `mypy` is (trivially) happy with the code if you simply don't annotate `x`. – chepner Feb 09 '21 at 16:51
  • Looking at this code again, I am not sure what you are trying to achieve anyway. Even for the types that do support ``cls.__new__(cls)`` (which are not all!) many will not support the ``self.__dict__`` and ``setattr(self, ...)``. Case in point, this cannot work for ``x: Any = 1`` – an integer has neither a dict nor would it support item assignment. – MisterMiyagi Feb 09 '21 at 18:15
  • @MisterMiyagi ok. In my actual use case, `x` is an instance of a specific custom class (or subclass thereof), that's typed to `Any` due to lack of third party type hints. I'll update the question – joel Feb 09 '21 at 18:29
  • 1
    Why don't you typehint ``x: Foo`` then, or even rely on ``x`` being inferred? ``Any`` will bring no advantage for type safety (it literally has no type information). – MisterMiyagi Feb 09 '21 at 18:41
  • @MisterMiyagi this is a simplified version. In the real thing, `x` is a parameter typed with `TypeVar("T", Foo)`, which I have assumed means it's essentially `Any` given `Foo` is untyped. I'll add the context in a bit – joel Feb 09 '21 at 20:56
  • Not sure how to relate the old and new parts – ``x`` (I assume it should be ``self`` actually) is already annotated via the signature. What you should annotate is ``new`` as ``new: T = cls.__new__(cls)``. – MisterMiyagi Feb 09 '21 at 21:35
  • @MisterMiyagi `x` should be `self` yes - copy paste error. the error remains if I annotate `new` – joel Feb 09 '21 at 23:05
  • How so? New is the only thing inferred to be Any. Do you still have an annotation for something else? It might be sensible to [edit] your question again so that there is only a single code block with well-defined issue. – MisterMiyagi Feb 10 '21 at 06:11
  • Sorry, it is getting increasingly difficult to work out what you are actually asking. Please [edit] your question to *reduce* the several different cases to one single, focused question. – MisterMiyagi Feb 10 '21 at 13:28
  • @MisterMiyagi the confusion is probably cos I didn't use a function. I've used that instead – joel Feb 10 '21 at 14:31

1 Answers1

5

The __ new __ suggestions you're getting from mypy are for the type class itself. You can see the constructors match perfectly: https://docs.python.org/3/library/functions.html#type

class type(object)
class type(name, bases, dict)

The complaint by mypy technically makes sense, because you're calling __ new __ from an object returned by type(). If we get the __ class __ of such an object (such as cls), we'll get <class 'type'>:

>>> x = [1,2,3,4]
>>> type(x)
<class 'list'>
>>> type(x).__class__
<class 'type'>

This might be what's tripping up mypy when T is unbounded (i.e. not specified at compile time). If you were inside a class, as you've noticed, and as mentioned in PEP 484 (https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods), it would be possible for mypy to discern the type as being the class of self, which is unambiguous.

With a standalone function, there are three approaches. One is to silence mypy directly with comment # type:ignore . The second is to grab __ class __ directly from x instead of using type(x), which generally returns the __ class __ anyway (see link above). The third is to use the fact that __ new __ is a class method and call it with x itself.

As long you want to use Any, there's no way to clarify to mypy that type(x) is anything other than Type[T`-1] while maintaining the generic nature (for instance, you could notate the cls = type(x) line with something like # type: List[int], but that would defeat the purpose), and it seems to be resolving the ambiguity with the return type of the type() command.

This coding works for a list (with a silly, element-wise list copy) and keeps me from getting any mypy errors:

from typing import TypeVar, Any, cast
T = TypeVar("T", bound=Any)

def foo(x: T) -> T:
    cls = type(x)
    res = cls.__new__(cls) # type: ignore
    for v in x:
      res.append(v)
    return res

x = [1,2,3,4]
y = foo(x)
y += [5]
print(x)
print(y)

Prints:

[1, 2, 3, 4]
[1, 2, 3, 4, 5]

Alternatively:

def foo(x: T) -> T:
    cls = x.__class__
    res = cls.__new__(cls)
    for v in x:
      res.append(v)
    return res

Third Approach:

from typing import TypeVar, Any
T = TypeVar("T", bound=Any)

def foo(x: T) -> T:
    res = x.__new__(type(x))
    for v in x:
      res.append(v)
    return res

x = [1,2,3,4]
y = foo(x)
y += [5]
print(x)
print(y)
Mark H
  • 4,246
  • 3
  • 12
  • 1
    Are you sure that is working, not just suppressing errors? It asserts that the *class* is a T, not its instance. – MisterMiyagi Feb 16 '21 at 06:47
  • Nah. That would for example assert that the list type is a list. It effectively says "is of itself". One casts a value as a type. – MisterMiyagi Feb 16 '21 at 06:58
  • I guess the ``cast(T, cls)`` works because ``__new__`` is a classmethod. So for example ``[].__new__`` and ``list.__new__`` are the same thing. – MisterMiyagi Feb 16 '21 at 08:58
  • Yes, I was thinking about that, but you're right that it's still technically incorrect to cast a type to the same type. I just added a note on using the __class__ attribute directly instead of going through type() to get it. – Mark H Feb 16 '21 at 09:25
  • 1
    `.__class__` fixes the error for me, thanks. What I'm surprised by is that `.__class__` and `type` don't do the same thing when called with a single object. And similarly that `type` doesn't have an (overloaded) definition of `type: T -> Type[T]` – joel Feb 17 '21 at 12:18
  • See especially the note on how both old- and new-style classes can have different results (tl;dr you can override the __class__ attribute): https://stackoverflow.com/questions/1060499/difference-between-typeobj-and-obj-class (Although in my python 3.8 I'm not seeing the old-style problem demonstrated) This is why I put the ignore recommendation first, because then at least you're still using type(). – Mark H Feb 17 '21 at 16:15
  • There are no old-style classes in Python3. It looks as if MyPy treats ``x.__class__`` and ``type(x)`` differently – the former is inferred, the latter typed. That is, mypy has some leeway in adjusting the type of ``x.__class__`` according to context. – MisterMiyagi Feb 17 '21 at 16:36
  • The good news is that, with either approach, using `reveal_type()` shows that mypy considers `y` to be of type `List[int]`, which means further type checking isn't impeded. It does still consider `res` to be of type `Any` though. – Mark H Feb 17 '21 at 19:59
  • I've upvoted and given you the bounty because this is helpful and solves my problem, but I'll leave it as unanswered because I'm still curious why `__class__` works when `type` doesn't – joel Feb 20 '21 at 12:39