pythonpython-typingmetaclass

How to type the __new__ method in a Python metaclass so that mypy is happy


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?


Solution

  • 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)