aio-libs/frozenlist

subscriptable typing

asmodehn opened this issue · 2 comments

Long story short

Subscripting the FrozenList type currently breaks.
I'm guessing a user would expect the FrozenList works the same as standard List as far as typing is concerned...

Example script:

from dataclasses import dataclass
from frozenlist import FrozenList

@dataclass
class MyClass:

    lst: FrozenList[int]


dc = MyClass(lst=FrozenList([1,2,3]))
print(dc)

Expected behaviour

I would expect this to work as with List:

from dataclasses import dataclass
from typing import List

@dataclass
class MyClass:

    lst: List[int]


dc = MyClass(lst=list([1,2,3]))
print(dc)
MyClass(lst=[1, 2, 3])

Actual behaviour

Traceback (most recent call last):
  File "fltest.py", line 5, in <module>
    class MyClass:
  File "fltest.py", line 7, in MyClass
    lst: FrozenList[int]
TypeError: 'type' object is not subscriptable

Your environment

$ python --version
Python 3.8.5
$ pip list | grep frozenlist
frozenlist                    1.1.1     

While I understand the details and reason of this behaviour, it caught me off-guard, so I thought I mention it.

Maybe the constructor and the type could be different (like list() / List) ?
Maybe also there could be a close match between List/FrozenList and the Set/FrozenSet with set()/frozenset() usage...

FrozenList works exactly like list here. You are confusing list, the concrete type, with typing.List, the type annotation generic.

The generic types from the typing module are not the same thing as the list or set concrete types, at least not unless you use Python 3.9, and use from __future__ import annotations.

You can see the difference in your own example:

class MyClass:

    lst: List[int]
    #    ^^^^ Capital L

dc = MyClass(lst=list([1,2,3]))
#                ^^^^ lowercase l

FrozenSet is not a generic type, it is a concrete class. If you tried to do the same with list (lst: list[int]) you'd get the same exception!

And while I understand that the naming convention for the generic typing versions is a source of confusion (using CamelCasing names where the built-in types use lowercase), FrozenSet should not be using lowercase naming just to avoid confusion here. With PEP 585 the core language is moving towards the concrete types supporting being used as generic type hints, and the confusion is only temporary.

The correct method is to either use from __future__ import annotations, or put the type in quotes as a forward reference. Both work.

With from __future__ import annotations:

from __future__ import annotations
from dataclasses import dataclass
from frozenlist import FrozenList

class MyClass:

    lst: FrozenList[int]

 
dc = MyClass(lst=FrozenList([1,2,3]))
print(dc)

or with a forward reference:

from __future__ import annotations
from dataclasses import dataclass
from frozenlist import FrozenList

class MyClass:

    lst: "FrozenList[int]"

 
dc = MyClass(lst=FrozenList([1,2,3]))
print(dc)

Both forms are recognized and supported by mypy as well.

That said, with Python 3.9 now implementing PEP 585, we could look into adding __class_getitem__ = classmethod(types.GenericAlias) to the class.