Using modern typing features on older versions of Python

1.6k Views Asked by At

So, I was writing an event emitter class using Python.

Code currently looks like this:

from typing import Callable, Generic, ParamSpec

P = ParamSpec('P')

class Event(Generic[P]):
  def __init__(self):
    ...

  def addHandler(self, action : Callable[P, None]):
    ...

  def removeHandler(self, action : Callable[P, None]): 
    ...

  def fire(self, *args : P.args, **kwargs : P.kwargs):
    ...

As you can see, annotations depend on ParamSpec, which was added to typing in python 3.10 only.

And while it works good in Python 3.10 (on my machine), it fails in Python 3.9 and older (on other machines) because ParamSpec is a new feature.

So, how could I avoid importing ParamSpec when running program or use some fallback alternative, while not confusing typing in editor (pyright)?

2

There are 2 best solutions below

1
On BEST ANSWER

I don't know if there was any reason to reinvent the wheel, but typing_extensions module is maintained by python core team, supports python3.7 and later and is used exactly for this purpose. You can just check python version and choose proper import source:

import sys

if sys.version_info < (3, 10):
    from typing_extensions import ParamSpec
else:
    from typing import ParamSpec
0
On

This could be solving by wrapping from typing import ... into if TYPE_CHECKING:

if TYPE_CHECKING:
    from typing import Callable, Generic, ParamSpec
else:
    # Fake ParamSpec
    class ParamSpec:
        def __init__(self, _):
            self.args = None
            self.kwargs = None
    # Base class to be used instead Generic
    class Empty:
        pass
    # Thing what returns Empty when called like Generic[P]
    class _Generic:
        def __getitem__(self, _):
            return Empty
    # Callable[anything] will return None
    class _Callable:
        def __getitem__(self, _):
            return None
    # Make instances
    Callable = _Callable()
    Generic = _Generic()

... code

if not TYPE_CHECKING:
    # To allow usage of
    # evt : Event[[int, str]]

    # Store existing Event class
    _Event = Event
    class FakeEvent:
        # Event(...) calls original constructor
        def __call__(self, *args, **kwds):
            return _Event(*args, **kwds)
        # Event[...] returns original Event
        def __getitem__(self, _):
            return _Event
    # Replace Event class
    Event = FakeEvent()

This allows code to be run with older versions of Python while using typing from 3.10 in the editor.