Mypy: filtering mapping of objects based on typing.Protocol as a TypeVar input

I’m trying to write a function that filters an input mapping to return a list of objects that implement a specific input protocol. The function looks like this:

from typing import TypeVar
from mypackage import ModelProtocol

P = TypeVar("P", bound=ModelProtocol)

def filter_models(
    models: Mapping[str, ModelProtocol],
    proto: type[P],
    choices: Sequence[str] | None = None,
) -> list[P]:
    """Filter 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)]

It’s usage should be:

myobjs = filter_models(models, CustomProtocol)

where models is of type Mapping[str, ModelProtocol]. It is always guaranteed that CustomProtocol inherits ModelProtocol.

mypy can correctly infer that myobjs is of type list[CustomProtocol] but then gives the following error:

Only concrete class can be given where "type[CustomProtocol]" is expected

Other than suppressing this error in the configuration, isn’t there any other way to express to mypy that this is fine?

You’ll need a TypeForm[P] instead of type. The difference is that type requires things to be an actual class (and in particular a non-abstract class which could have its constructor called), while TypeForm allows any valid type. The PEP’s in draft state, but an experimental implementation was added to the development builds of Mypy.

Even with TypeForm, the implementation of your function is unlikely to pass type checking. You’ll need to check the object passed in is actually a protocol, and I’d probably recommend not relying on isinstance(x, protocol), that just checks each method exists, it’s not particularly robust.

1 Like

That should be easily fixable, thanks for pointing that out.

I haven’t looked hard enough for a suitable alternative but it seems to me that to enforce a more strict check there’s not a lot that I can do, or is there some built-in or 3rd party solution that provides a more robust check on that?

To do it robustly you’d need to examine the signatures of methods, which involves essentially implementing parts of a type checker to resolve generic types and match arguments. There are libraries that do this in general like typeguard, trycast and beartype, but given the complexity of functions in Python, it looks like they all only have basic support right now.

I don’t know your context, probably a simpler check would be sufficient.