I'm experiencing a type checking error with Pyright when using a decorator with a method that overrides another method. The error occurs only when the method has two parameters (self
and another parameter), but disappears when I define it so that there's only one parameter (self
).
Here`s my code with errors:
from functools import wraps
from typing import Callable, Concatenate, ParamSpec, TypeVar
T = TypeVar("T")
P = ParamSpec("P")
R = TypeVar("R")
# Here wrapper has channel: str
def use_channel(
func: Callable[Concatenate[T, str, P], R]
) -> Callable[Concatenate[T, str, P], R]:
@wraps(func)
def wrapper(self: T, channel: str, *args: P.args, **kwargs: P.kwargs) -> R:
print(f"[Decorator] self={self}, channel={channel}")
return func(self, channel, *args, **kwargs)
return wrapper
class A:
def set_channel(self, channel: str):
pass
class B(A):
@use_channel
# It result in Pyright Error
def set_channel(self, channel: str):
pass
This code displays no errors:
from functools import wraps
from typing import Callable, Concatenate, ParamSpec, TypeVar
T = TypeVar("T")
P = ParamSpec("P")
R = TypeVar("R")
# Here wrapper only has self argument
def use_channel(
func: Callable[Concatenate[T, P], R]
) -> Callable[Concatenate[T, P], R]:
@wraps(func)
def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> R:
print(f"[Decorator] self={self}")
return func(self, *args, **kwargs)
return wrapper
class A:
def set_channel(self, channel: str):
pass
class B(A):
@use_channel
# No Error
def set_channel(self, channel: str):
pass
The error message is:
Method "set_channel" overrides class "A" in an incompatible manner
Parameter 2 mismatch: base parameter "channel" is keyword parameter, override parameter is position-only
Can someone explain why having one or two parameters makes a difference here? Is this related to how Pyright checks compatibility between methods?"
If you use Callable[Concatenate[T, str, P], R]
it means that T, str
are positional only as per the specification of PEP 612.
Hence your decorator returns a function with positional-only arguments, those are not compatible with the super class that allows positional or keyword parameters:
A solution is to modify your parent/interface class to only accept positional only attributes in that case:
class A:
def set_channel(self, channel: str, /): # <-- mark channel positional only with /
pass
class B(A):
@use_channel # OK
def set_channel(self, channel: str):
pass
This will already silence your errors with pyright. mypy will report the error in your decorator instead, to not report an error you should include the positional /
in your wrapper.
def wrapper(self: T, channel: str, /, *args: P.args, **kwargs: P.kwargs) -> R:
The interesting thing - depending whether you include /
- in your decorator is if B().set_channel(channel="sdf")
is runtime-valid or not, for the type-checker it is an error.