Here is some mock code:
def my_func() -> set[tuple[str, ...]]:
my_set: set[tuple[str, str]] = set()
# do some stuff
return my_set
Running mypy on above code gives the error:
Incompatible return value type (got "Set[Tuple[str, str]]", expected "Set[Tuple[str, ...]]")
Why is set[tuple[str, str]]
not recognized as a set[tuple[str, ...]]
?
In production, I have several functions of the above form, and each function returns a set of homogenous tuples (my_func2
would return a set of 5-tuples, and my_func3
would return a set of 11-tuples
). I would like the return type to be the same for my family of functions my_func, my_func2, my_func3
.
set
is invariant, meaning that the type parameters have to match exactly, rather than one simply conforming to the other. The simplest fix here is to just annotate my_set
with the desired return type:
def my_func() -> set[tuple[str, ...]]:
my_set: set[tuple[str, ...]] = set()
my_set.add(("foo", "bar"))
return my_set
Another option (if you want my_set
to be limited to 2-element tuples within the body of the function) would be to cast
it when you return:
from typing import cast
def my_func() -> set[tuple[str, ...]]:
my_set: set[tuple[str, str]] = set()
my_set.add(("foo", "bar"))
return cast(set[tuple[str, ...]], my_set)
However, note that the reason that sets are invariant is that they are mutable. Suppose you pass my_set
(as a set of 2-tuples) to another (more picky) function that expects it to only ever contain 2-tuples, and then return a reference to my_set
that's been cast so that it can contain n-tuples. Now anyone with that cast reference can insert n-tuples into it, which violates the typing assumptions of the more picky function! This is why cast
can be dangerous -- in this case it's safe only as long as no reference to my_set
with its 2-tuple typing persists after the return
.
So if you want my_set
to be typed as a 2-tuple, but you want to return a set of n-tuples, another option is to return a frozenset
:
def my_func() -> frozenset[tuple[str, ...]]:
my_set: set[tuple[str, str]] = set()
my_set.add(("foo", "bar"))
return frozenset(my_set)
Returning a frozenset
ensures that the caller will not be able to mutate my_set
and violate its 2-tuple typing.
Note that returning an anonymous (mutable) copy is also perfectly safe (I think), but mypy won't deduce that, so you still have to cast
it explicitly:
return cast(set[tuple[str, ...]], my_set.copy())