Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type a function that takes a tuple and returns a tuple of the same length with each element optional?

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]]].

like image 504
tibbe Avatar asked Nov 18 '25 17:11

tibbe


1 Answers

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.

like image 122
Daniil Fajnberg Avatar answered Nov 21 '25 06:11

Daniil Fajnberg