I want to write a function that takes a tuple and returns a tuple of the same size but with each element wrapped in optional. Pseudo code:
from typing import TypeVar
T = TypeVar("T", bound=tuple[dict[str, str], ...])
def f(tup: T) -> Map[Optional, T]:
# Dummy implementation
return [None if ... else el for el in tup]
Here Map is a made-up, type-level function that wraps each returned element's type in Optional.
Concretely, if the input type was e.g. tuple[dict[str, str], dict[str, str]] I want the return type to be tuple[Optional[dict[str, str]], Optional[dict[str, str]]].
TL;DR: You can't, unless you specify its size explicitly.
You are asking about type annotations that take into account tuple size, yet you use code examples that specifically disregard tuple size.
The usage of literal ellipsis indicates to the static type checker that the tuple can be of any undefined size. (see docs)
Also annotating *args to a function with T is equivalent to declaring args to be of type tuple[T, ...].
For the tuple size to be considered relevant by a static type checker, you need to explicitly define its element types:
from typing import Optional, TypeVar
T = TypeVar("T")
def f(t: tuple[T, T, T]) -> tuple[Optional[T], Optional[T], Optional[T]]:
return None, t[1], t[2]
Or, if they are different:
from typing import Optional, TypeVar
T1 = TypeVar("T1")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
def g(t: tuple[T1, T2, T3]) -> tuple[Optional[T1], Optional[T2], Optional[T3]]:
return None, t[1], t[2]
If your tuple can be of any size and its elements have the same type T, then you can merely indicate that the returned tuple can also have any size (i.e. with no regard for the size of the argument tuple) with its element type being Optional[T], as you already realized:
def h(t: tuple[T, ...]) -> tuple[Optional[T], ...]:
return t + (t[0], )
PS
If you want to abuse typing.Literal, then a very crude custom generic subclass of tuple that receives a specific length as an additional type argument as well as requiring the length to be specified and consistent in the constructor is possible, but looks quite ugly IMO.
from __future__ import annotations
from collections.abc import Iterable
from typing import Generic, Literal, Optional, TypeVar
E = TypeVar("E")
L = TypeVar("L", bound=int)
class MyTuple(Generic[E, L], tuple[E, ...]):
def __new__(cls, iterable: Iterable[E], length: L) -> MyTuple[E, L]:
iterable = tuple(iterable)
if len(iterable) != length:
raise RuntimeError
return tuple.__new__(cls, iterable)
T = TypeVar("T")
L3 = Literal[3]
def func(t: MyTuple[T, L3]) -> MyTuple[Optional[T], L3]:
return MyTuple((None, t[1], t[2]), length=3)
Changing that func return annotation to MyTuple[Optional[T], Literal[2]] would cause a mypy error:
error: Argument "length" to "MyTuple" has incompatible type "Literal[3]"; expected "Literal[2]" [arg-type]
Also changing the argument for lenght from 3 to 2 would cause an analogous error (just the other way around).
But there is no way for a static type checker to notice an inconsistency between the explicit length and the actual number of elements produced by the iterable passed to the constructor.
So again, all in all, not a great solution. But I thought it was interesting and somewhat relevant.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With