I've contributed type hints for ordered-set library. The problem is that despite I have the following lines in ordered_set.pyi
file:
from typing import MutableSet, TypeVar, Sequence
T = TypeVar('T')
class OrderedSet(MutableSet[T], Sequence[T]):
...
I cannot write:
IntOrderedSet = OrderedSet[int]
in my code, Python raises:
TypeError: 'ABCMeta' object is not subscriptable
It happens because in the ordered_set.py
OrderedSet
is defined as:
from collections.abc import MutableSet, Sequence
class OrderedSet(MutableSet, Sequence):
...
I made a PR with commit that changes OrderedSet
class to inherit from typing classes, but ordered-set
owner refused to accept it, because as he said:
Importing typing inside the code adds an install-time dependency and a run-time cost to the code. ordered_set has gotten by as a single module with no dependencies for 6 years. Someone could decide they want nothing to do with setuptools and just drop ordered_set on their PYTHONPATH, and it would still work. I'd hate to lose that over types.
Is there a way, to support OrderedSet[int]
like types without modifying the library ordered_set.py
file?
The only workaround that I'm aware of is to define any custom type aliases such that they exist only at type-check time, and never at run-time.
In order to do this, you would need to:
if typing.TYPE_CHECKING:
blocks. The typing.TYPE_CHECKING
block is always false at runtime, and treated as being true by type checkers.from __future__ import annotations
, which does that automatically. That is, avoid having to ever evaluate type hints in the first place.So for example, in your specific setup, you could contribute stubs for the OrderedSet library somewhere then write your code to look like this (assume Python 3.7+):
from __future__ import annotations
from typing import TYPE_CHECKING
from ordered_set import OrderedSet
# Basically equivalent to `if False`
if TYPE_CHECKING:
IntOrderedSet = OrderedSet[Int]
def expects_int_ordered_set(x: IntOrderedSet) -> None:
# blah
some_ordered_set: IntOrderedSet = OrderedSet()
Or, if you're using Python 3.6 or lower:
from typing import TYPE_CHECKING
from ordered_set import OrderedSet
if TYPE_CHECKING:
IntOrderedSet = OrderedSet[Int]
def expects_int_ordered_set(x: 'IntOrderedSet') -> None:
# blah
some_ordered_set: 'IntOrderedSet' = OrderedSet()
If you're ok with lying a little, we could dispense with the string stuff and define IntOrderedSet
to be slightly different things at type-checking time vs runtime. For example:
from typing import TYPE_CHECKING
from ordered_set import OrderedSet
if TYPE_CHECKING:
IntOrderedSet = OrderedSet[Int]
else:
IntOrderedSet = OrderedSet
def expects_int_ordered_set(x: IntOrderedSet) -> None:
# blah
some_ordered_set = IntOrderedSet()
Make sure to be careful when you're doing this though -- the type checker won't check anything in the 'else' block/won't check to make sure whatever you're doing there is consistent with what's in the if TYPE_CHECKING
block.
The final solution would be to just not define an IntOrderedSet
type in the first place. This lets us skip hack 1 while only needing to use hack 2 (which isn't so much a hack -- it'll be default behavior in Python in a few years).
So for example, we could do this:
from __future__ import annotations
from ordered_set import OrderedSet
def expects_int_ordered_set(x: OrderedSet[int]) -> None:
# blah
some_ordered_set: OrderedSet[int] = OrderedSet()
Depending on context, it may be the case that I don't even need to add an annotation to that last variable declaration. Here, we do, but if we were defining that variable inside of a function, it's possible type checkers like mypy would automatically infer the correct type for us based on how we end up using that variable.