Nick
Nick

Reputation: 22175

Can mypy generics express passing the return sequence type as an argument?

I'd like to write the following generic Python code:

from itertools import chain
from typing import Sequence, Hashable, List, Tuple, Type, TypeVar


SequenceT = TypeVar('SequenceT', bound=Sequence)
HashableT = TypeVar('HashableT', bound=Hashable)


def merge_and_sort_into(seq_type, *iterables):
    # type: (Type[SequenceT], *Iterable[HashableT]) -> SequenceT[HashableT]
    return seq_type(sorted(set(chain(*iterables))))


def merge_and_sort_into_list(*iterables):
    # type: (*Iterable[HashableT]) -> List[HashableT]
    return merge_and_sort_into(list, *iterables)


def merge_and_sort_into_tuple(*iterables):
    # type: (*Iterable[HashableT]) -> Tuple[HashableT]
    return merge_and_sort_into(tuple, *iterables)

The code is fine, but mypy doesn't like the return type of merge_and_sort_into() saying error: Type variable "SequenceT" used with arguments. How can I pass a Sequence type into a function and also use that type as the return type? (note: I'm not capturing the fact that the value type for the sequence also needs to be comparable/sortable, but let's just ignore that).

Here is a version that mypy will accept, but it doesn't capture the constraint that type passed into merge_and_sort_into() is its return type, hence the casts.

def merge_and_sort_into(seq_type, *iterables):
    # type: (Callable[[Iterable[HashableT]], Sequence[HashableT]], *Iterable[HashableT]) -> Sequence[HashableT]
    return seq_type(sorted(set(chain(*iterables))))


def merge_and_sort_into_list(*iterables):
    # type: (*Iterable[HashableT]) -> List[HashableT]
    return cast(List[HashableT], merge_and_sort_into(list, *iterables))


def merge_and_sort_into_tuple(*iterables):
    # type: (*Iterable[HashableT]) -> Tuple[HashableT]
    return cast(Tuple[HashableT], merge_and_sort_into(tuple, *iterables))

Upvotes: 2

Views: 1455

Answers (1)

jirassimok
jirassimok

Reputation: 4253

Mypy does not support generic type variables because they would require the type system to support higher kinds, as discussed in this comment.

Even if Mypy supported generic type variables, the type signature of the original function would be incorrect, because not all Sequences can be constructed from an iterable. For example,

class EmptyList(Sequence[T]):
  def __init__(self): pass
  def __getitem__(self, item): raise IndexError
  def __len__(self): return 0

EmptyList([1, 2, 3]) # TypeError

The most straightforward solution for your specific case is probably to simply allow non-sequence return types, and use Callable instead of Type.

T, R = TypeVar('T'), TypeVar('R')

def chain_into(into: Callable[[Iterable[T]], R], *iters: Iterable[T]) -> R:
    return into(chain.from_iterable(iters))

Upvotes: 2

Related Questions