Abstract, generic class properties beyond Python 3.13

Up until now (Python 3.12) I’ve been using this pattern to get the concrete type out of an abstract generic at run time:

DataIn = TypeVar('DataIn', infer_variance=True)


class Handler(Generic[DataIn], ABC):
    @classmethod
    @property
    def processable(cls) -> type[DataIn]:
        return get_args(get_original_bases(cls)[0])[0]

    @abstractmethod 
    def handle(self, msg: Msg[DataIn]):
        ...

And it all type checks and works great, but:

# Class properties are deprecated in Python 3.11 and will not be supported in Python 3.13

Some searching and I’ve found two possible solutions, but neither seem satisfactory:


Option 1

Use a metaclass, thus:

class HandlerMeta(ABCMeta):
    @property
    def processable(cls):
        return get_args(get_original_bases(cls)[0])[0]


@dataclass
class Handler(Generic[DataIn], metaclass=HandlerMeta):

But as you can see I had to remove the reference to DataIn, because generics aren’t allowed in metaclasses, which means the caller sees:

# error: Cannot access member "processable" for type "Handler[Processable@Dispatcher]"
#    Member "processable" is unknown (reportAttributeAccessIssue)

Option 2

I also tried __init_subclass__, thus:

@classmethod
def __init_subclass__(cls, /, **kwargs: Any):
    cls.processable: type[DataIn] = get_args(get_original_bases(cls)[0])[0]

But then the caller gets:

# error: Access to generic instance variable through class is ambiguous (reportGeneralTypeIssues)

Any ideas?

What’s preventing making processable either an instance property or a regular class method?

Oh, yes, well, I suppose I should have included that as a workaround.

But I do like it being a class property, and was hoping there is a way to keep it so in 3.13 onwards.

I assume not for very long, because get_original_bases was only added in 3.12 in the first place.

To be clear, for those of us in the back who have been happily ignoring all the typing features: the goal seems to be, to be able to do something like

class IntHandler(Generic[int], Handler):
    def handle(self, msg: Msg[int]):
        ...

and have it type-check in your IDE, and also be able to assert IntHandler.processable is int at runtime?

But - what does the latter piece of information gain you at runtime?

If you don’t already know which Handler subtype you’re using at that point in the process, then presumably you want to use some kind of dispatching system. But that would have to have been set up earlier in the code. If you want that to happen automatically - say, at the time that IntHandler is created - then decorators are perfectly capable of, ahem, handling that.

Well spotted! Yes I was doing cls.__orig_bases__ before that. A little gross, but it worked and I’d seen get_original_bases was coming; it’s actually only switching to that which got me looking at the code and saw the deprecation warning :crazy_face:

Yeah I’ve got a messaging queue, and handlers which look like this:

Processable = Foo | Bar

class FooBarHandler(Handler[Processable]):
    def handle(self, msg: Msg[Processable]):

        match (payload := msg.data):
            case Foo():
                ...

Then as messages come off the queue I can check processable to see if a handler handles it, and be safe in the knowledge that the match would fail with reportMatchNotExhaustive if the implementation was missing.


I’d probably just accept making processable a class method before I refactored to use decorators, but if you have an example of a dispatching with decorators somewhere I’d love to take a look.

Thanks for the reply :pray:t2:

Off the top of my head:

handler_map : dict[type, Handler] = {}

def register(cls):
    handler_map[get_args(get_original_bases(cls)[0])[0]] = cls
    return cls

@register
class IntHandler(Generic[int], Handler):
    def handle(self, msg: Msg[int]):
        ...

Now at runtime when you discover the type being handled you can use it as a key in handler_map to get the class, and instantiate and use that.

That isn’t going to work with type checking, of course; information that you only get at runtime is inherently information that can’t be used before then. If you know ahead of time that you want to handle an int then it doesn’t make any sense to expect the runtime to discover IntHandler for you dynamically while also expecting the type checker to know that the runtime will do so. Just specify IntHandler directly.

2 Likes

Have you tried defining your own descriptor class? What’s been deprecated is specifically classmethod + property, but you could define your own class with a __get__() method. That could itself be generic.

1 Like