How can I type hint a dynamically set class attribute in a metaclass?

Since you define the field on the class, the practical approach is to type-hint the field. Note that you must tell mypy to not check the line itself, though.

class Person(PersonBase):
    age: int = IntField()  # type: ignore

This is the least change, but rather inflexible.


You can create automatically typed, generic hints by using a helper function with a fake signature:

from typing import Type, TypeVar


T = TypeVar('T')


class __Field__:
    """The actual field specification"""
    def __init__(self, *args, **kwargs):
        self.args, self.kwargs = args, kwargs


def Field(tp: Type[T], *args, **kwargs) -> T:
    """Helper to fake the correct return type"""
    return __Field__(tp, *args, **kwargs)  # type: ignore


class Person:
    # Field takes arbitrary arguments
    # You can @overload Fields to have them checked as well
    age = Field(int, True, object())

This is how the attrs library provides its legacy hints. This style allows to hide all the magic/hacks of the annotations.


Since a metaclass can inspect annotations, there is no need to store the type on the Field. You can use a bare Field for metadata, and an annotation for the type:

from typing import Any


class Field(Any):  # the (Any) part is only valid in a .pyi file!
    """Field description for Any type"""


class MetaPerson(type):
    """Metaclass that creates default class attributes based on fields"""
    def __new__(mcs, name, bases, namespace, **kwds):
        for name, value in namespace.copy().items():
            if isinstance(value, Field):
                # look up type from annotation
                field_type = namespace['__annotations__'][name]
                namespace[name] = field_type()
        return super().__new__(mcs, name, bases, namespace, **kwds)


class Person(metaclass=MetaPerson):
    age: int = Field()

This is how the attrs provides its Python 3.6+ attributes. It is both generic and conforming to annotation style. Note that this can also be used with a regular baseclass instead of a metaclass.

class BasePerson:
     def __init__(self):
         for name, value in type(self).__dict__.items():
             if isinstance(value, Field):
                 field_type = self.__annotations__[name]
                 setattr(self, name, field_type())


class Person(BasePerson):
    age: int = Field()

Patrick Haugh is right, I am trying to solve this the wrong way. Descriptors are the way to go:

from typing import TypeVar, Generic, Optional, ClassVar, Any, Type

FieldValueType = TypeVar('FieldValueType')


class Field(Generic[FieldValueType]):

    value_type: Type[FieldValueType]

    def __init__(self) -> None:
        self.value: FieldValueType = self.value_type()

    def __get__(self, obj, objtype) -> 'Field':
        print('Retrieving', self.__class__)
        return self

    def __set__(self, obj, value):
        print('Updating', self.__class__)
        self.value = value

    def to_string(self):
        return self.value

class StringField(Field[str]):
    value_type = str

class IntField(Field[int]):
    value_type = int

    def to_string(self):
        return str(self.value)


class Person:
    age = IntField()

person = Person()
person.age = 25
print(person.age.to_string())

MyPy can fully understand this. Thanks!