Skip to content

Use case for typing.Type with abstract types #4717

Description

@erikwright

In #2169 and #1843 there was discussion about using Type[some_abc] and how it should be allowed in a function signature, but that the call-site was expected to pass a concrete subclass of some_abc. There is an implicit assumption that the objective of a function taking such a type is, ultimately, to instantiate the type.

@gvanrossum said, in #2169 (comment):

But maybe we need a check that you call this thing with a concrete subclass;
and for that we would need some additional notation (unless maybe we could
just say that whenever there's an argument of Type[A] where A is abstract,
that the argument must be a concrete subclass. But for that we'd need some
experiment to see if there's much real-world code that passes abstract
classes around. (If there is, we'd need to have a way to indicate the need
in the signature.)

I have such a use case.

I have a sequence of observers supplied by clients of my library, to which I want to dispatch events according to the abstract base class(es) that each implements. I tried to do this as follows:

import abc
import typing

class FooObserver(metaclass=abc.ABCMeta):
    """Receives Foo events."""

    @abc.abstractmethod
    def on_foo(self, count: int) -> None:
        raise NotImplementedError()

class BarObserver(metaclass=abc.ABCMeta):
    """Receives Bar events."""

    @abc.abstractmethod
    def on_bar(self, status: str) -> None:
        raise NotImplementedError()

class Engine:
    def __init__(self, observers: typing.Sequence[typing.Any]) -> None:
        self.__all_observers = observers

    def do_bar(self, succeed: bool) -> None:
        status = 'ok' if succeed else 'problem'
        for bar_observer in self.__observers(BarObserver):
            bar_observer.on_bar(status)

    def do_foo(self, elements: typing.Sequence[typing.Any]) -> None:
        count = len(elements)
        for foo_observer in self.__observers(FooObserver):
            foo_observer.on_foo(count)

    __OBSERVER_TYPE = typing.TypeVar('__OBSERVER_TYPE')

    def __observers(
            self,
            observer_type: typing.Type['__OBSERVER_TYPE']
    ) -> typing.Sequence['__OBSERVER_TYPE']:
        return [observer for observer in self.__all_observers
                if isinstance(observer, observer_type)]

Unfortunately, MyPy complains about this as follows:

/Users/erikwright/abc_typing.py:24: error: Only concrete class can be given where "Type[BarObserver]" is expected
/Users/erikwright/abc_typing.py:29: error: Only concrete class can be given where "Type[FooObserver]" is expected

Given that (AFAICT) the decision was made to not attempt to verify that the runtime type supports any specific constructor signature I'm wondering why there is nonetheless an expectation that the runtime type is constructable at all. In my case, the entire purpose of typing here is:

  1. Require you to actually pass a type, which means I can use it in isinstance
  2. Allow me to specify the return type of the method in terms of the supplied type.

Activity

  1. ilevkivskyi commented on Mar 17, 2018

    @ilevkivskyi
    Member

    The point is that it is too hard to track which classes are instantiated in the body of a given function, and which are not. If we would have such tracking, then we could allow calling functions with abstract classes at particular positions.

    Taking into account that such tracking would take some effort, and that during the year of current behaviour, this is a first such request, I would recommend just using # type: ignore. Even if this will be implemented at some point, this is quite low priority.

  2. glyph commented on Jan 15, 2019

    @glyph

    As far as I can tell, the same problem applies to Protocol as well. Is there any way to have a TypeVar that references anything abstract?

  3. JukkaL commented on Jan 15, 2019

    @JukkaL
    Collaborator

    @glyph Perhaps you could use # type: ignore to silence the error as suggested above? It's clearly not optimal, though. Can you give more information about your use case?

    I wonder if we could enable Type[x] with abstract and protocol types but disallow creating an instance (i.e they wouldn't be callable). They could still be used for things like isinstance checks.

  4. glyph commented on Jan 15, 2019

    @glyph

    What I’m trying to do is to write a class decorator, @should_implement(SomeProtocol) which type-checks the decorated class to ensure it complies with the given protocol, so ignoring the error would obviate the whole point ;-).

  5. JukkaL commented on Jan 15, 2019

    @JukkaL
    Collaborator

    Makes sense, though I'm not sure if lifting the restriction would be sufficient to allow the decorator to be checked statically. Right now a runtime check is probably the best you can do with that syntax, at least without a plugin. For a static check you could use a dummy assignment (which is not very pretty).

    Increasing priority to normal since I think that the current approach is too restrictive. I'm not sure what's the best way to move forward, though.

  6. glyph commented on Jan 15, 2019

    @glyph

    @JukkaL 🙏

  7. glyph commented on Jan 16, 2019

    @glyph

    @JukkaL I think I found a workaround, leveraging the little white lie that Protocols without constructors are callables that return themselves:

    from typing import Callable, Type, TypeVar
    from typing_extensions import Protocol
    
    class AProtocol(Protocol):
        x: int
    
    
    protocol = TypeVar("protocol")
    def adherent(c: Callable[[], protocol]) -> Callable[[Type[protocol]], Type[protocol]]:
        def decor(input: Type[protocol]) -> Type[protocol]:
            return input
        return decor
    
    
    @adherent(AProtocol)            # No error; Yes is the expected shape
    class Yes(object):
        x: int
        other: str
    
    y = Yes()
    y.x
    y.other
    
    
    @adherent(AProtocol)            # We get an error here, as desired
    class No(object):
        y: int
  8. glyph commented on Aug 6, 2019

    @glyph

    I should note that there's a big problem with my workaround; you can only apply the decorator once, and then it breaks down. There's a variant here where you can abuse a Generic instead, and then write

    Adherent[AProtocol](Yes)
    Adherent[AProtocol](No)
    

    but these have to come after the class body and look somewhat uglier.

  9. ilevkivskyi commented on Aug 7, 2019

    @ilevkivskyi
    Member

    I'm not sure what's the best way to move forward, though.

    A possible ad-hoc solution (which may be not so bad), is to just remove this check for class decorators, because it also causes troubles for dataclasses, see #5374 that has 12 upvotes.

  10. petr-motejlek commented on Feb 1, 2020

    @petr-motejlek

    This "Only concrete class can be given" error also seems impossible to overcome for code that is supposed to accept an abstract class and return some instance of that class, even if it would have to create the class right then and there using some fancy mechanism like type(a, b, c). Think unittest.Mock and similar dummy object factories.

    I have also just realized that this breaks (mypy raises this error) even when you write a function that acts like isinstance(...) with the provided class. Makes you wonder how isinstance is actually typed in typeshed (gonna check that now).

    from abc import ABC
    from typing import TypeVar, Type
    
    T = TypeVar('T')
    
    class Abstract(ABC):
      pass
    
    def isthisaninstance(this, type_: Type[T]) -> bool:
      return isinstance(this, type_)
    
    isthisaninstance("", Abstract)   # type: ignore :(

    Is there any way to overcome this (other than to # type: ignore all function/method calls?

  11. JukkaL commented on Feb 4, 2020

    @JukkaL
    Collaborator

    Is there any way to overcome this

    In some cases you may use if TYPE_CHECKING: to conditionally define the base class so that mypy sees the base class as object while at runtime it will be ABC:

    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        Base = object
    else:
        Base = ABC
    
    class Abstract(Base):
        pass

    You'll lose any ABC checking by mypy, however.

  12. 85 remaining items

  13. rilshok commented on May 19, 2025

    @rilshok

    type-abstract should be disabled.

    The annotation type[T] describes a class object, not whether it can be instantiated. Rejecting abstract classes here conflates type identity with instantiability, which are orthogonal concerns.

    Many valid use cases, such as dependency injection, plugin registration, and factory patterns, require passing abstract types without instantiating them. This check blocks these patterns unnecessarily and forces developers to use # type: ignore, which defeats the purpose of static typing.

    If instantiability needs to be enforced, it should be done explicitly (e.g., via Callable[..., T]), not assumed by type[T].

    Allowing abstract classes in type[T] improves flexibility, reduces false positives, and aligns better with real-world Python usage.

  14. dpinol commented on Oct 6, 2025

    @dpinol

    def get_concrete[T: type[ABC]](interface: T) -> T: ... does not work in my case. I still get Only concrete class can be given where
    Any other workaround?

    What is weird is that def cast(typ: type[_T], val: Any) -> _T function itself does not have this restriction about the classes not being abstract

  15. added a commit that references this issue on Oct 21, 2025
  16. vallsv commented on Nov 7, 2025

    @vallsv

    What is weird is that def cast(typ: type[_T], val: Any) -> _T function itself does not have this restriction about the classes not being abstract

    Interesting.

    If anybody have a work around, it would be very convenient.

  17. jacopoabramo commented on Dec 10, 2025

    @jacopoabramo
    Contributor

    Adding my use case to the choir: I've been struggling for a bit with a relatively small function that filters an object based on an input protocol. A minimal example:

    from typing_extensions import TypeVar, Mapping, Protocol, Sequence, runtime_checkable
    
    @runtime_checkable
    class ModelProtocol(Protocol):
        """A protocol that all models should implement."""
        @property
        def name(self) -> str:
            ...
    
    @runtime_checkable
    class SubModelProtocol(ModelProtocol, Protocol):
        """A more specific protocol that some models may implement."""
        def specific_method(self) -> None:
            ...
    
    P = TypeVar("P", bound=ModelProtocol)
    
    def filter_models(
        models: Mapping[str, ModelProtocol],
        proto: type[P],
        choices: Sequence[str] | None = None,
    ) -> list[P]:
        """Filter a map of models by a specific protocol type.
    
        Parameters
        ----------
        models : ``Mapping[str, ModelProtocol]``
            Mapping of model names to model instances.
        proto : ``type[Any]``
            The protocol type to filter for.
        choices : ``Sequence[str]``, optional
            If provided, return only models associated with names in this sequence.
            Default is ``None`` (all ``proto`` models are returned).
    
        Returns
        -------
        list[P]
            List of model instances that implement the given protocol.
        """
        if choices is not None:
            return [
                model
                for name, model in models.items()
                if isinstance(model, proto) and name in choices
            ]
        return [model for model in models.values() if isinstance(model, proto)]
    
    class ExampleModel:
        """An example model implementing ModelProtocol."""
        def __init__(self, name: str):
            self._name = name
    
        @property
        def name(self) -> str:
            return self._name
        
    class ExampleSubModel:
        """An example sub-model implementing SubModelProtocol."""
        def __init__(self, name: str):
            self._name = name
    
        @property
        def name(self) -> str:
            return self._name
    
        def specific_method(self) -> None:
            print(f"Specific method called on {self._name}")
    
    
    objs: Mapping[str, ModelProtocol] = {
        "model1": ExampleModel("Model 1"),
        "submodel1": ExampleSubModel("SubModel 1"),
    }
    
    filtered = filter_models(objs, SubModelProtocol)
    
    assert len(filtered) == 1

    The key problem is that isinstance refuses to acknowledge proto as correct input type, even though by having a look around it is what some runtime type checkers also use for their checks.

    I opened a discussion in the Python discourse where it was told me that PEP 747 (TypeForm) might be a solution, but for the life of me I can't figure out how to use it and it seems it is not supposed to be used with isinstance.

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