Example Code
I = TypeVar("I", bound=Optional[Iterable])
O = TypeVar("O", bound=Optional[Mappable])
class Worker(Generic[I, O]):
@abstractmethod
def do_work(self, input: I) -> O:
pass
worker = Worker[list, dict]()
worker_with_optional = Worker[Optional[list], Optional[dict]]()
worker_bad_types = Worker[Optional[list], dict]()
The actual code in the example may seem a bit contrived, but it was the best way I could think to abstractly represent my problem. What I’m struggling to do is set up TypeVars
for the input to Generic
that would allow users to create worker
and worker_with_optional
but not worker_bad_types
.
The concept that I’m struggling to put into code is the relatedness of the two input types. I want to require that I
and O
are both Optional or they are both required to have a value.
The best workaround for the functionality I want would be to make two versions of the class like this:
I = TypeVar("I", bound=Iterable)
O = TypeVar("O", bound=Mappable)
class Worker(Generic[I, O]):
@abstractmethod
def do_work(self, input: I) -> O:
pass
class WorkerWithOptional(Generic[I, O]):
@abstractmethod
def do_work(self, input: Optional[I]) -> Optional[O]:
pass
worker = Worker[list, dict]()
worker_with_optional = WorkerWithOptional[list, dict]()
# worker_bad_types now has a type error because None is not Iterable
worker_bad_types = Worker[Optional[int], str]()
This approach accomplishes the type restrictions I want, but I’d rather not make two copies of my class to account for this. Is there a way to relate the Optionality of the types passed into Generic
so that they always match?
I = TypeVar("I", bound=Iterable)
O = TypeVar("O", bound=Mapping)
T = TypeVar("T", None, Never)
class BaseWorker(Generic[I, O, T]):
@abstractmethod
def do_work(self, input: I | T) -> O | T:
pass
Worker: TypeAlias = BaseWorker[I, O, Never]
WorkerWithOptional: TypeAlias = BaseWorker[I, O, None]
Never
is uninstantiable, meaning that I | Never
simplifies to I
. We take advantage of this to allow your optionality to be controlled by the parameter T
.
If you run the mypy playground of the below code, you can see that of the 4 test cases, only #2 receives a type error as expected.
class TestWorker(Worker[list[int], dict[int, int]]):
def do_work(self, input: list[int]) -> dict[int, int]:
return {x: 1 for x in input}
class TestWorkerWithOptional(WorkerWithOptional[list[int], dict[int, int]]):
def do_work(self, input: list[int] | None) -> dict[int, int] | None:
return None if input is None else {x: 1 for x in input}
worker = TestWorker()
worker_w_opt = TestWorkerWithOptional()
worker.do_work([1]) # 1
worker.do_work(None) # 2
worker_w_opt.do_work([1]) # 3
worker_w_opt.do_work(None) # 4