Search code examples
pythonmypy

mypy return type of tuples of various length


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.


Solution

  • 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())