Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Classmethods on generic classes

I try to call a classmethod on a generic class:

from typing import List, Union, TypeVar, Generic
from enum import IntEnum

class Gender(IntEnum):
    MALE = 1
    FEMALE = 2
    DIVERS = 3


T = TypeVar('T')

class EnumAggregate(Generic[T]):
    def __init__(self, value: Union[int, str, List[T]]) -> None:
        if value == '':
            raise ValueError(f'Parameter "value" cannot be empty!')

        if isinstance(value, list):
            self._value = ''.join([str(x.value) for x in value])
        else:
            self._value = str(value)

    def __contains__(self, item: T) -> bool:
        return item in self.to_list

    @property
    def to_list(self) -> List[T]:
        return [T(int(character)) for character in self._value]

    @property
    def value(self) -> str:
        return self._value

    @classmethod
    def all(cls) -> str:
        return ''.join([str(x.value) for x in T])

Genders = EnumAggregate[Gender]

But if I call

Genders.all()

I get the error TypeError: 'TypeVar' object is not iterable. So the TypeVar T isn't properly matched with the Enum Gender.

How can I fix this? The expected behavior would be

>>> Genders.all()
'123'

Any ideas? Or is this impossible?

like image 491
DarkMath Avatar asked Nov 01 '25 18:11

DarkMath


1 Answers

Python's type hinting system is there for a static type checker to validate your code and T is just a placeholder for the type system, like a slot in a template language. It can't be used as an indirect reference to a specific type.

You need to subclass your generic type if you want to produce a concrete implementation. And because Gender is a class and not an instance, you'd need to tell the type system how you plan to use a Type[T] somewhere, too.

Because you also want to be able to use T as an Enum() (calling it with EnumSubclass(int(character))), I'd also bind the typevar; that way the type checker will understand that all concrete forms of Type[T] are callable and will produce individual T instances, but also that those T instances will always have a .value attribute:

from typing import ClassVar, List, Union, Type, TypeVar, Generic
from enum import IntEnum

T = TypeVar('T', bound=IntEnum)  # only IntEnum subclasses

class EnumAggregate(Generic[T]):
    # Concrete implementations can reference `enum` *on the class itself*,
    # which will be an IntEnum subclass.
    enum: ClassVar[Type[T]]

    def __init__(self, value: Union[int, str, List[T]]) -> None:
        if not value:
            raise ValueError('Parameter "value" cannot be empty!')

        if isinstance(value, list):
            self._value = ''.join([str(x.value) for x in value])
        else:
            self._value = str(value)

    def __contains__(self, item: T) -> bool:
        return item in self.to_list

    @property
    def to_list(self) -> List[T]:
        # the concrete implementation needs to use self.enum here
        return [self.enum(int(character)) for character in self._value]

    @property
    def value(self) -> str:
        return self._value

    @classmethod
    def all(cls) -> str:
        # the concrete implementation needs to reference cls.enum here
        return ''.join([str(x.value) for x in cls.enum])

With the above generic class you can now create a concrete implementation, using your Gender IntEnum fitted into the T slot and as a class attribute:

class Gender(IntEnum):
    MALE = 1
    FEMALE = 2
    DIVERS = 3


class Genders(EnumAggregate[Gender]):
    enum = Gender

To be able to access the IntEnum subclass as a class attribute, we needed to use typing.ClassVar[]; otherwise the type checker has to assume the attribute is only available on instances.

And because the Gender IntEnum subclass is itself a class, we need to tell the type checker about that too, hence the use of typing.Type[].

Now the Gender concrete subclass works; the use of EnumAggregate[Gender] as a base class tells the type checker to substitute T for Gender everywhere, and because the implementation uses enum = Gender, the type checker sees that this is indeed correctly satisfied and the code passes all checks:

$ bin/mypy so65064844.py
Success: no issues found in 1 source file

and you can call Genders.all() to produce a string:

>>> Genders.all()
'123'

Note that I'd not store the enum values as strings, but rather as integers. There is little value in converting it back and forth here, and you are limiting yourself to enums with values between 0 and 9 (single digits).

like image 169
Martijn Pieters Avatar answered Nov 03 '25 08:11

Martijn Pieters