Search code examples
pythoninheritancemypypython-typing

MyPy displays an error when inheriting from str and adding metaclass


Here's simple example of code:

class meta(type): pass
class Test(str, metaclass = meta): pass

When I run mypy on it (just from cli, without any flags or other additional setup) I'm seeing next output:

test.py:2: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases  [misc]

I know that similar error appears at runtime when metaclass of derived class is not a subtype of all parents metaclasses. But type(str) is type, and my class meta derived from type, so I cannot understand what's incorrect here.

Provided example isn't working only with str parent class. Mypy says everything is correct when inheriting from int or float.

So, how to get rid of such error message? Or it is just a mypy bug?


Solution

  • str is statically-typed as a subclass of typing.Protocol, whose metaclass is special-cased by mypy as typing._ProtocolMeta (typing.Protocol is actually an instance of typing._ProtocolMeta at runtime, but this isn't reflected in the typing stubs, so normally mypy wouldn't know about this).

    Therefore, to accurately reflect both static typing and runtime in your example, you can do this (mypy Playground, Pyright Playground):

    import typing as t
    
    if t.TYPE_CHECKING:
        from typing import _ProtocolMeta
    else:
        _ProtocolMeta = type(str)
    
    class meta(_ProtocolMeta): pass
    class Test(str, metaclass=meta): pass
    

    Since typing._ProtocolMeta has a bare minimum typing interface, this leaves you relatively free to implement meta without too much maintenance if type-checkers and/or the Python runtime decides to change the metaclass of str in the future.

    Note that keyword arguments in a class declaration do not currently work properly in mypy for user-defined metaclasses, so you'll run into some type-safety issues for those. Instead of using metaclasses, consider using a base class defining __init_subclass__ instead.