Every now and then, I happen to work with fixed-size homogenous collections… and find no satisfying way to annotate them.
Let’s say I have a function to compute in-place a Fibonacci generalized sequence depending on the first two terms:
def compute_fibonacci_terms(seq, n_terms): a, b = seq for _ in range(n_terms): a, b = b, a + b seq.append(b) return seq
>>> compute_fibonacci_terms([3, 4], n_terms=5) [3, 4, 7, 11, 18, 29, 47]
To annotate this function, the two choices I see are
def compute_fibonacci_terms(seq: list[int], n_terms: int) -> list[int]: ...
def compute_fibonacci_terms(seq: tuple[int, int], n_terms: int) -> list[int]: ...
The first solution allows to pass list with any number of items, resulting in a
ValueError in this (admittedly contrived) example. The second solution is even worse, disallowing passing lists (which is why this function was designed that way) and rightly reporting an error on
Even when working with tuples, the situation is not ideal when it comes to large tuples:
def get_weekday_names(locale: str) -> tuple[str, str, str, str, str, str, str]: ...
Here type-checking works well, but is quite cumbersome to read and error-prone.
What I’d like to propose here is to overload the
__mul__ operator of
type to denote fixed size homogenous collections.
So the above functions could be written as
def compute_fibonacci_terms(seq: list[int * 2], n_terms: int) -> list[int]: ...
def get_weekday_names(locale: str) -> tuple[str * 7]: ...
type (as well as of some typing special forms usable as type annotations) would define
__mul__ to ony accept an integer and return some special typing construct, similar to
typing.GenericAlias. Type checkers would only allow literal integers.
I believe type checkers should only allow this syntax inside generic types derived of
tuple[int, int, int] and
tuple[int * 3] should be considered fully equivalent.
type and typing special constructs cannot currently be multiplied by anything, this should be fully backwards compatible. One case that could be problematic are defered anotations, since
list["LateBind" * 3] would become
list["LateBindLateBindLateBind"] , but I believe type checkers can just flag this as invalid (and suggest using
list["LateBind * 3"] instead).
Future improvements this may open the way for...
- Typing of heterogenous sequences (
list[int * 3, str, int * 2]), even it I find that less readable and probably a way less common use case;
- Bounded iterable size: for example,
itertools.teecould have an overload
or even, to get back to my example,
def tee[I, N: Literal[int]](iterable: Iterable[I], n: N=2) -> tuple[I * N]: ...
compute_fibonacci_terms[N: Literal[int]](seq: list[int * 2], n_terms: N) -> list[int * (N + 2)]: ...
- Some way to denote “at least X” / “at most X” items ?
Has something in these lines been discussed before? I’m pretty sure I read a discussion about this issue somewhere here, but I couldn’t find it and I don’t think it really had a proposal.
arg: int * 3could be made sugar for
arg: Collection[int * 3], but I’m not sure I’m fan of the idea. ↩︎