Using python code typed with ParamSpec on older versions of Python

Viewed 281

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):
    ...

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 not import ParamSpec when running program, while not confusing typing in editor (pyright)?

2 Answers

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

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.

Related