11

I am trying to type the __new__ method in a metaclass in Python so that it pleases mypy. The code would be something like this (taken from pep-3115 - "Metaclasses in Python 3000" and stripped down a bit):

from __future__ import annotations

from typing import Type


# The metaclass
class MetaClass(type):

    # The metaclass invocation
    def __new__(cls: Type[type], name: str, bases: tuple, classdict: dict) -> type:
        result = type.__new__(cls, name, bases, classdict)
        print('in __new__')
        return result


class MyClass(metaclass=MetaClass):
    pass

With this, mypy complains, Incompatible return type for "__new__" (returns "type", but must return a subtype of "MetaClass"), pointing at the line def __new__.

I have also tried with:

def __new__(cls: Type[MetaClass], name: str, bases: tuple, classdict: dict) -> MetaClass:

Then mypy complains (about the return result line): Incompatible return value type (got "type", expected "MetaClass").

I have also tried with a type var (TSubMetaclass = TypeVar('TSubMetaclass', bound='MetaClass')) and the result is the same as using MetaClass.

Using super().__new__ instead of type.__new__ gave similar results.

What would be the correct way to do it?

wjandrea
  • 28,235
  • 9
  • 60
  • 81

1 Answers1

9

First, the return type is MetaClass, not type. Second, you need to explicitly cast the return value, since type.__new__ doesn't know it is returning an instance of MetaClass. (Its specific return type is determined by its first argument, which isn't known statically.)

from __future__ import annotations

from typing import Type, cast


# The metaclass
class MetaClass(type):

    # The metaclass invocation
    def __new__(cls: Type[type], name: str, bases: tuple, classdict: dict) -> MetaClass:
        result = type.__new__(cls, name, bases, classdict)
        print('in __new__')
        return cast(MetaClass, result)


class MyClass(metaclass=MetaClass):
    pass

To use super, you need to adjust the static type of the cls parameter.

class MetaClass(type):

    # The metaclass invocation
    def __new__(cls: Type[MetaClass], name: str, bases: tuple, classdict: dict) -> MetaClass:
        result = super().__new__(name, bases, classdict)
        print('in __new__')
        return cast(MetaClass, result)
Óscar López
  • 232,561
  • 37
  • 312
  • 386
chepner
  • 497,756
  • 71
  • 530
  • 681
  • That seems like a hack - for example, that signature would prevent you from using `super()`, since the 2nd arg would not be an instance of the 1st. Well, to be honest, `cast` seems generally wrong in Python typing, like a lie. It seems to me that if we start with something like `def __new__(cls: Type[MetaClass [...]`, then `type.__new__(cls [...]` should understand that it is returning an instance of `MetaClass`. So I'll wait to see if there is some solution w/o using `cast` before accepting your answer. Thanks! – Enrique Pérez Arnaud Jul 23 '20 at 14:13
  • 1
    You're not wrong. I did notice the issue with `super`, which is why I didn't include it in the answer. – chepner Jul 23 '20 at 15:16
  • The problem with `super` can be resolved by changing the annotation of `cls` to `Type[MetaClass]`, but I think that has its own issues if you have a metaclass hierarchy. (I shudder at the thought of anything that complex, though.) – chepner Jul 23 '20 at 15:19
  • 2
    Mmm yes, if we change the `cls` annotation to `Type[MetaClass]` *and* we cast the result, we can use `super()` and it pleases mypy... I really wish there was a solution w/o casting, but for now I'll accept your answer :) – Enrique Pérez Arnaud Jul 23 '20 at 15:34
  • I'd like to see the answer patched to include the proper `super()` call. – jsbueno Jul 23 '20 at 16:42