pythondecoratorpython-typingpython-decoratorspyright

Why does Pyright report an incompatible method override when using a decorator with multiple parameters?


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?"


Solution

  • 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.