'MyPy doesn't seem to pick up on a class's attributes when altering said attributes w/ a metaclass

So I've got something like this:

from dataclasses import dataclass, field
from typing import ClassVar

import itertools


class MetaComponent(type):
    iterator: ClassVar[itertools.count] = itertools.count(1)

    def __new__(cls, clsname, bases, attrs):
        attrs["type_of_part_index"] = next(MetaComponent.iterator)
        return super().__new__(cls, clsname, bases, attrs)


class Component(metaclass=MetaComponent):
    type_: str = field(init=False)
    singleton: bool = True

    def __post_init__(self):
        self.type_ = type(self).__name__.lower()


@dataclass
class Tire(Component):
    radius: float
    singleton: bool = False


@dataclass
class Body(Component):
    color: str


body = Body(color="red")
print(body.type_of_part_index)

MyPy gives me "Body" has no attribute "type_of_part_index". The code works as intended, so I'm wondering if there's a bug w/ MyPy or am I doing something incorrect w.r.t. adding an attribute like this. Is there a fix for this, or some more canonical way of writing the code such that MyPy sees the attribute correctly?



Solution 1:[1]

According to docs,

Mypy supports the lookup of attributes in the metaclass

but

Mypy does not and cannot understand arbitrary metaclass code.

The most common solution is to declare attribute type (either on metaclass or on derived class). Declaring on metaclass is better for DRY (no need to repeat this with each derived class), but worse for typechecking: trying to access MetaComponent.type_of_part_index will be an error not visible to mypy. Declaring in derived class is opposite.

So you can do this:

class MetaComponent(type):
    iterator: ClassVar[itertools.count] = itertools.count(1)
    type_of_part_index: int  # Will be cls attribute
    # or ClassVar[int] or whatever you need

    def __new__(cls, clsname, bases, attrs):
        attrs["type_of_part_index"] = next(MetaComponent.iterator)
        return super().__new__(cls, clsname, bases, attrs)

or this:

class Component(metaclass=MetaComponent):
    type_of_part_index: int  # Added by metaclass

    type_: str = field(init=False)
    singleton: bool = True

    def __post_init__(self):
        self.type_ = type(self).__name__.lower()

In this situation I would prefer the latter (and comment), because metaclass isn't reused.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 SUTerliakov