Typing instance based on ClassVar

This library has recently added typing, and you can see a good artifact of a previous design choice causing complication / confusion.

It used a CONSUME_MULTIPLE_SEGMENTS class variable to decide whether an abstract convert method should receive str or list[str].

It’s currently incorrectly hard coded to str, which is a bug:
Typing for BaseConverter convert doesn't respect CONSUME_MULTIPLE_SEGMENTS · Issue #2396 · falconry/falcon · GitHub.

Simple fix is to replace str with str | list[str], but then the implementor needs to assert the concrete type, even though it is known based on the value of CONSUME_MULTIPLE_SEGMENTS.

I assume there’s no way to type convert based on the ClassVar, but I wanted to check :pray:t2:

Not certain it will work, but perhaps worth trying is to use a union of two narrow definitions instead of a base class.

class SingleConverter(Protocol):
    CONSUME_MULTIPLE_SEGMENTS: Literal[False]
    def convert(self, value: str, /) -> Any: ...

class MultiConverter(Protocol):
    CONSUME_MULTIPLE_SEGMENTS: Literal[True]
    def convert(self, value: Sequence[str], /) -> Any: ...

Converter: TypeAlias = SingleConverter | MultiConverter 

Usually this pattern also works well in conjunction with having a (private) base class if needed.

1 Like

I haven’t tried it, but depending on how CONSUME_MULTIPLE_SEGMENTS is set (by sub-classes?) you might be able to make this work using some trickery using Generic:

# untested

_B = TypeVar("_B", bool, default=False)

class BaseConverter(Generic[_B]):
    CONSUME_MULTIPLE_SEGMENTS: ClassVar[_B] = False

    @overload
    @abc.abstractmethod
    def convert(self: Self[False], value: str) -> Any: ...
    @overload
    @abc.abstractmethod
    def convert(self: Self[True], value: list[str]) -> Any: ...
    def convert(self: Self[True], value: list[str]) -> Any: pass

class ConverterImpl(BaseConverter[True]):
    CONSUME_MULTIPLE_SEGMENTS = True

    # probably won't work easily
    def convert(self: Self[True], value: list[str]) -> Any:
        pass

Alternatively, in sub-classes just define the allowed version and # type: ignore the overload errors.

I’m not sure that any of those solutions (or rather “hacks”) really works for your use case.

Sadly, type variables are not allowed in ClassVars: Class type assignability — typing documentation

Note that a ClassVar parameter cannot include any type variables, regardless of the level of nesting: ClassVar[T] and ClassVar[list[set[T]]] are both invalid if T is a type variable.

1 Like