10

I want to be able to define what the contents of a subclass of list have to be. The class would look like the following.

class A(list):
   def __init__(self):
      list.__init__(self)

I want to include typing such that the following would happen.

import typing

class A(list: typing.List[str]):  # Maybe something like this
   def __init__(self):
      list.__init__(self)

>> a = A()
>> a.append("a")  # No typing error
>> a.append(1)  # Typing error
13
  • 2
    You want generic types. Note, python never enforces your type annotations. You can use third party static type checkers like mypy though Commented Feb 27, 2019 at 20:35
  • Basically, you need to implement the MutableSequence protocol yourself. I would not inherit from list, as A should be usable anywhere an ordinary list is, and that means potentially accepting things like myAobject.append("foo"). Have A include a list as an instance variable instead instead of inheriting from list, and let the defined methods enforce the int-only restriction. Commented Feb 27, 2019 at 20:45
  • (Sorry, I was thinking of list-of-ints in my previous comment.) Commented Feb 27, 2019 at 20:52
  • @chepner But if I want all of the same methods as list in my subclass, why not just use list? Commented Feb 27, 2019 at 21:03
  • 2
    @Interlooper Say A is supposed to be a list of ints. The Liskov substitution principle says that anywhere I might want to use an instance of list, I should be able to use an instance of A in its place. But if A is (e.g.) supposed to be a list of ints, that means the following is invalid: a = A(); a.append("foo"). A list is a mutable sequence of arbitrary objects; A is not, so it should not be a subclass of list. It doesn't matter if the two classes have the same interface, because the semantics are very different. Commented Feb 27, 2019 at 21:07

2 Answers 2

7

typing conveniently provides a generic version of collections.MutableSequence, so something to the effect of:

import typing

T = typing.TypeVar('T')
class HomogeneousList(typing.MutableSequence[T]):
    def __init__(self, iterable: typing.Iterable[T]=()) -> None:
        self._data: typing.List[T]  = []
        self._data.extend(iterable)

    @typing.overload
    def __getitem__(self, index: int) -> T: ...
    @typing.overload
    def __getitem__(self, index: slice) -> HomogeneousList[T]: ...
    def __getitem__(self, index):
        return self._data[index]

    @typing.overload
    def __setitem__(self, index: int,  item: T) -> None: ...
    @typing.overload
    def __setitem__(self, index: slice, item: typing.Iterable[T]) -> None: ...
    def __setitem__(self, index, item):
        self._data[index] = item

    def __delitem__(self, index: typing.Union[int, slice]) -> None:
        del self._data[index]

    def __len__(self) -> int:
        return len(self._data)

    def insert(self, index: int, item: T) -> None:
        self._data.insert(index, item)


string_list = HomogeneousList[str]()
string_list.append('foo')
string_list.append(42)


int_list = HomogeneousList[int]()
int_list.append(42)
int_list.append('foo')

Now, mypygives the following errors:

test.py:36: error: Argument 1 to "append" of "MutableSequence" has incompatible type "int"; expected "str"
test.py:41: error: Argument 1 to "append" of "MutableSequence" has incompatible type "str"; expected "int"

There is some tricky aspects of typing __getitem__ etc because they accept slice objects as well, but not terrible.

Note, this is useful, because if you just try to do:

class HomogeneousList(collections.abc.MutableSequence, typing.Generic[T]):
    ....

MyPy, at least, doesn't throw an error for append. AFAIKT you'd have to explicitly add:'

def append(self, item: T) -> None:
    self._data.append(item)

Which sort of removes a lot of the utility of collections.abc.MutableSequence to begin with. Anyway, thankfully, typing provides generic versions of all of these out of the box!

Note, you can use these generically, like I've show, but you can also do something like:

class StringList(HomogeneousList[str]):
    pass

mylist = StringList([1,2,3]) # mypy error
mylist = StringList('abc') # no error

mylist.append('foo') # no error
mylist.append(42) # mypy error
Sign up to request clarification or add additional context in comments.

Comments

5

Prior to Python 3.9, you can use:

import typing

class A(typing.List[str]):
    pass

This indicates to your type checker that elements of class A should be of type str. At run-time, this behaves the same as creating a subclass of list. PEP 484 specifies how the typing system behaves. In particular, the example in this section of the PEP does something similar to what you're asking, but with typing.Dict instead of typing.List.

In Python 3.9+, you can use the built-in type instead of importing from typing. The class becomes:

class A(list[str]):
    pass

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.