Skip to content

"Dynamic metaclass not supported" false positive on generics #11672

Open

Description

@DetachHead
from typing import TypeVar, Generic

T = TypeVar("T")

class FooMetaclass(type, Generic[T]): pass

class Foo(metaclass=FooMetaclass[T]): # error: Dynamic metaclass not supported for "Foo"
    pass

https://mypy-play.net/?mypy=master&python=3.10&gist=d4b169a5ece08ed0607b19fff8e9b749

according to the documentation:

Mypy does not understand dynamically-computed metaclasses, such as class A(metaclass=f()): ...

however this isn't dynamically computed, it's just a generic so mypy should be able to understand it statically

Activity

  1. sobolevn commented on Dec 8, 2021

    @sobolevn
    Member

    I am not sure that generic metaclasses is even a thing 🤔
    I've never seen any references in PEPs / docs. Can you please check that it can be used this way?

  2. DetachHead commented on Dec 8, 2021

    @DetachHead
    ContributorAuthor

    pyright supports it and it works at runtime, so i see no reason these two features shouldn't work together for this to emerge.

    from typing import TYPE_CHECKING, Generic, TypeVar
    
    T = TypeVar("T")
    
    
    class FooMetaclass(type, Generic[T]):
        value: T
    
    
    class Foo(metaclass=FooMetaclass[int]):  # mypy error, no pyright error
        @classmethod
        def foo(cls) -> None:
            if TYPE_CHECKING:
                reveal_type(cls.value)  # pyright says `int`, mypy says `Any`
            cls.value = 1
            print(cls.value)
    
    
    Foo.foo()  # prints 1
    print(Foo.__orig_class__)  # __main__.FooMetaclass[int]
  3. erictraut commented on Dec 9, 2021

    @erictraut

    Pyright should generate an error in the first example above. The fact that it doesn't is an oversight / bug. FooMetaclass[T] shouldn't be allowed because TypeVar T doesn't have a defined scope in this case.

    Pyright does support the use of a specialized metaclass, as in your second example above. I don't see any reason why this shouldn't work from a type-checking standpoint. That said, it's a use case that isn't well tested, so you may run into issues.

    There's an interesting case not covered in either of the above two samples:

    from typing import TypeVar, Generic
    
    T = TypeVar("T")
    
    class FooMetaclass(type, Generic[T]):
        pass
    
    class Foo(Generic[T], metaclass=FooMetaclass[T]):
        pass

    In this variant, TypeVar T does have a well-defined scope (since it's bound to class Foo), so theoretically, this variant could be valid. I'm not sure it's something we'd want to support though.

    @DetachHead, is there a real use case here, or are you just playing around with ideas?

  4. DetachHead commented on Dec 9, 2021

    @DetachHead
    ContributorAuthor

    Pyright does support the use of a specialized metaclass, as in your second example above. I don't see any reason why this shouldn't work from a type-checking standpoint. That said, it's a use case that isn't well tested, so you may run into issues.

    yeah i found this issue:

    class FooMetaclass(type, Generic[T]):
        metaclass_value: T
    
    
    class Foo(metaclass=FooMetaclass[int]):
        value: int
    
    
    # wrong error message:
    # Cannot assign member "value" for type "Type[Foo]"
    #   Member "value" is unknown
    Foo.metaclass_value = "1"
    
    # it should be more like this error instead:
    # Cannot assign member "value" for type "Type[Foo]"
    #   Expression of type "Literal['1']" cannot be assigned to member "value" of class "Foo"
    #     "Literal['1']" is incompatible with "int"
    Foo.value = "1"

    There's an interesting case not covered in either of the above two samples:

    from typing import TypeVar, Generic
    
    T = TypeVar("T")
    
    class FooMetaclass(type, Generic[T]):
        pass
    
    class Foo(Generic[T], metaclass=FooMetaclass[T]):
        pass

    In this variant, TypeVar T does have a well-defined scope (since it's bound to class Foo), so theoretically, this variant could be valid. I'm not sure it's something we'd want to support though.

    yeah that's what i intended to put as my original example, i just forgot to include Generic[T] in the bases.

    however it doesn't seem to work in this case:

    class FooMetaclass(type, Generic[T]):
        metaclass_value: T
    
    
    class Foo(Generic[T], metaclass=FooMetaclass[T]):
        value: T
    
    
    # should be `int` but is still the unbound generic
    reveal_type(Foo[int].metaclass_value)  # Type of "Foo[int].metaclass_value" is "T@Foo"
    
    Foo[int].metaclass_value = 1  # error
    
    Foo[int].value = 1 # no error

    @DetachHead, is there a real use case here, or are you just playing around with ideas?

    i stubled across this while trying to learn how metaclasses work, but i was basically trying to make a ReifiedGeneric class that looks something like this:

    class _ReifiedGenericMetaclass(Generic[T], type):
        __generics__: T
        def __instancecheck__(
            cls,
            obj: object,
        ) -> bool:
            ...
    
    
    class ReifiedGeneric(Generic[T], metaclass=_ReifiedGenericMetaclass[T]):
        def __class_getitem__(
            cls,
            item: T,
        ) -> type[ReifiedGeneric[T]]:
            ...
  5. last-partizan commented on Jun 10, 2022

    @last-partizan

    Those dymanic metaclasses would be really useful for factory-boy. I'm trying to add typing support for it, and i've managed to get if work for some cases, but one unsolved issue remains, here's minimal example based on factory_boy/factory/base.py

    from typing import Generic, TypeVar
    
    T = TypeVar("T")
    
    
    class FactoryMetaClass(type):
        """Factory metaclass for handling ordered declarations."""
    
        def __call__(cls, **kwargs):
            """Override the default Factory() syntax to call the default strategy.
    
            Returns an instance of the associated class.
            """
            return cls.create(**kwargs)  # type: ignore
    
    
        def __new__(cls, class_name, bases, attrs):
            return super().__new__(cls, class_name, bases, attrs)
    
    
    class BaseFactory(Generic[T]):
        """Factory base support for sequences, attributes and stubs."""
    
        @classmethod
        def create(cls, **kwargs) -> T:
            ...
    
    
    class Factory(BaseFactory[T], metaclass=FactoryMetaClass):
        """Factory base with build and create support.
    
        This class has the ability to support multiple ORMs by using custom creation
        functions.
        """
    
    # ------------------------------
    
    class Book:
        name: str
        author: str
    
    
    class BookFactory(Factory[Book]):
        ...
    
    
    book1 = BookFactory.create()
    reveal_type(book1)
    
    book2 = BookFactory()
    reveal_type(book2)

    Both mypy and pyright correctly handling first case, but i have no idea how to get it to return Book for second case.

    mypy basic_example.py
    basic_example.py:40:13: note: Revealed type is "basic_example.Book*"
    basic_example.py:43:13: note: Revealed type is "basic_example.BookFactory"
    

    @erictraut maybe you have idea how to solve this without generic metaclasses?

  6. erictraut commented on Jun 10, 2022

    @erictraut

    maybe you have idea how to solve this without generic metaclasses?

    Yeah, use classes rather than metaclasses. This is the normal way to implement the factory pattern.

    from typing import Generic, TypeVar
    
    T = TypeVar("T")
    
    class Factory(Generic[T]):
        def __init__(self, cls: type[T]):
            self.cls = cls
    
        def create(self, *args, **kwargs):
            return self.cls(*args, **kwargs)
    
        def __call__(self, *args, **kwargs):
            return self.create(*args, **kwargs)
    
    class Book:
        name: str
        author: str
    
    BookFactory = Factory(Book)
    
    book1 = BookFactory.create()
    reveal_type(book1)
    
    book2 = BookFactory()
    reveal_type(book2)
  7. erdnaxeli commented on Aug 21, 2023

    @erdnaxeli

    That's not how FactoryBoy works. It works like this:

    class BookFactory(factory.Factory):
        ...
    
    book1 = Bookfactory()

    You don't instantiate the factory.

  8. STofone commented on May 10, 2025

    @STofone

    I need classproperty with generic. And i've found so many needs for Generic metaclass.

    from functools import cached_property
    from typing import Callable, Literal, LiteralString, Type
    
    
    class SetFunc[T: LiteralString]:
        def __init__(self, id: T):
            self.id = id
    
        def __call__(self, func: Callable):
            ...
            return func
    
    class META_A[T: LiteralString](type):
        @cached_property
        def set_func(cls) -> Type[SetFunc[T]]:
            ...# extend SetFunc and change META_A class attributes in __init__ of SetFunc
            return SetFunc
    
    class A[T: LiteralString](metaclass=META_A[T]):
        pass
    
    class B(A[Literal["1", "2", "3"]]):
        pass
    
    # type checking for id does not work with pyright
    @B.set_func("1")
    def func():
        pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions