You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ParamSpec incompatible with union of callables #4760
ParamSpec seems to be incompatible with a Union[Callable]
from typing import Callable, ParamSpec, TypeAlias, TypeVar, Union, cast
FnWithOptionalArg: TypeAlias = Union[
Callable[[], int],
Callable[[int], int],
]
fn = cast(FnWithOptionalArg, lambda: 1)
P = ParamSpec("P")
R = TypeVar("R")
def my_decorator(fn: Callable[P, R]) -> Callable[P, R]:
return fn
my_decorator(fn) # ERROR
# :16:14 - error: Argument of type "FnWithOptionalArg" cannot be assigned to parameter "fn" of type "(**P@my_decorator) -> R@my_decorator" in function "my_decorator"
# Type "FnWithOptionalArg" cannot be assigned to type "(int) -> int"
# Type "() -> int" cannot be assigned to type "(int) -> int"
# Function accepts too many positional parameters; expected 0 but received 1 (reportGeneralTypeIssues)
T_Callable = TypeVar("T_Callable", bound=Callable)
def my_decorator_2(fn: T_Callable) -> T_Callable:
return fn
my_decorator_2(fn) # OK
Expected behavior
I would expect my_decorator(fn) above to not raise a type error and to return a value of FnWithOptionalArg, which is what my_decorator_2(fn) does.
If your code relies on symbols that are imported from a third-party library, include the associated import statements and specify which versions of those libraries you have installed.
VS Code extension or command-line
command-line, 1.1.298-- previously I was on 1.1.293 and this error did not occur.
I think pyright's behavior is correct here. A ParamSpec captures the signature of a function. It is considered invariant, so it must capture the same signature wherever it's used. You're attempting to assign two different signatures to it (one that accepts an int parameter and one that doesn't).
[ParamSpec] is considered invariant, so it must capture the same signature wherever it's used. You're attempting to assign two different signatures to it (one that accepts an int parameter and one that doesn't).
Thanks for the response. Confused by the above statement. A regular TypeVar is also invariant by default-- so by your stated logic I would expect to see a similar error if we "attempt to assign two different [types]" to R. But this doesn't raise an error:
from typing import Callable, TypeVar, Union, cast
from typing_extensions import TypeAlias, ParamSpec
FnWithOptionalArg: TypeAlias = Union[
Callable[[], int],
Callable[[], str],
]
fn = cast(FnWithOptionalArg, lambda: 1)
P = ParamSpec("P")
R = TypeVar("R")
def my_decorator(fn: Callable[P, R]) -> Callable[P, R]:
return fn
x = my_decorator(fn) # OK; as expected, type is `() -> int | str`
Why does the behavior of an invariant ParamSpec not match the behavior of an invariant TypeVar here?
A regular TypeVar can take on the union of two types (or the "join" of two types in the case of mypy). A ParamSpec cannot take on multiple signatures. There's no way to "widen" a signature to include multiple signatures.
Think about what it would mean if unions of signatures were possible. How would the final type Callable[P, R] be evaluated if P were allowed to contain a union of different signatures? There is no concept in the Python type system of a callable that accepts a union of different input signatures. There is a concept of an overloaded signature. I recently added the ability for a ParamSpec to capture an overloaded signature. (Note: mypy doesn't have such support, and it's not clear from the PEP that it should be supported.) Perhaps an overload would satisfy your use case?
The other thing that you could do is define FnWithOptionalArg as a callback protocol that accepts an optional parameter. This way, you don't need to use a union to describe the type.
The type errors surfaced because we are passing a CallbackType as the wrapped arg to functools.update_wrapper, which apparently uses ParamSpec and started raising a type errors when we bumped Pyright to 1.1.298.
I experimented a little and from what I can tell, a callback protocol can't correctly express CallbackType:
from typing import Any, Callable, TypeVar
from typing_extensions import ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
T_Callable = TypeVar("T_Callable", bound=Callable[..., Any])
def dec_outer(f: T_Callable) -> T_Callable:
return dec_inner(f) # error
# /Users/smackesey/stm/code/elementl/oss/test.py:9:12 - error: Expression of type "(...) -> Any" cannot be assigned to return type "T_Callable@dec_outer"
# Type "(...) -> Any" cannot be assigned to type "T_Callable@dec_outer" (reportGeneralTypeIssues)
def dec_inner(f: Callable[P, R]) -> Callable[P, R]:
return f
Why the error here? Shouldn't pyright understand that dec_inner returns a function with the same type as it takes in? Or am I missing something about how ParamSpec works?
ParamSpec captures the signature of a function that is assigned to it. In your case, you're passing a TypeVar (T_Callable), so there is no signature to capture. The best a type checker can do is use the signature from the TypeVar's bound, which is .... That means the result of calling dec_inner is (...) -> Any. This isn't compatible with the return type T_Callable because the relationship with the original TypeVar is lost when you pass it through a function.
Activity
erictraut commentedon Mar 10, 2023
I think pyright's behavior is correct here. A
ParamSpeccaptures the signature of a function. It is considered invariant, so it must capture the same signature wherever it's used. You're attempting to assign two different signatures to it (one that accepts anintparameter and one that doesn't).Mypy generates the same error in this case.
smackesey commentedon Mar 10, 2023
Thanks for the response. Confused by the above statement. A regular
TypeVaris also invariant by default-- so by your stated logic I would expect to see a similar error if we "attempt to assign two different [types]" toR. But this doesn't raise an error:Why does the behavior of an invariant
ParamSpecnot match the behavior of an invariantTypeVarhere?erictraut commentedon Mar 10, 2023
A regular
TypeVarcan take on the union of two types (or the "join" of two types in the case of mypy). AParamSpeccannot take on multiple signatures. There's no way to "widen" a signature to include multiple signatures.erictraut commentedon Mar 10, 2023
Think about what it would mean if unions of signatures were possible. How would the final type
Callable[P, R]be evaluated ifPwere allowed to contain a union of different signatures? There is no concept in the Python type system of a callable that accepts a union of different input signatures. There is a concept of an overloaded signature. I recently added the ability for a ParamSpec to capture an overloaded signature. (Note: mypy doesn't have such support, and it's not clear from the PEP that it should be supported.) Perhaps an overload would satisfy your use case?The other thing that you could do is define
FnWithOptionalArgas a callback protocol that accepts an optional parameter. This way, you don't need to use a union to describe the type.smackesey commentedon Mar 10, 2023
Thanks for your answers and suggestions. Your explanation of why there can't be unions of signatures makes sense. I'm a bit unclear on this though:
Is the correct way to express an overloaded signature as a type to use a callback protocol?
To provide a little more context-- the functions we are operating on come from our users. The function can optionally take a first argument:
We've been expressing the type of
user_fn_*as:The type errors surfaced because we are passing a
CallbackTypeas thewrappedarg tofunctools.update_wrapper, which apparently usesParamSpecand started raising a type errors when we bumped Pyright to 1.1.298.I experimented a little and from what I can tell, a callback protocol can't correctly express
CallbackType:I also tried combining a callback protocol with overloads (something I'm not even sure works ever):
So it doesn't look like anything can work here-- would you agree that at this time, it just isn't possible to properly express this:
In a way that is compatible with
ParamSpecas used infunctools.update_wrapper?erictraut commentedon Mar 10, 2023
Can you provide a more complete example that shows how you're using
CallbackTypewithfunctools.wrapscurrently?Is it something like this?
If so, then I agree with your conclusion that
functools.wrapswon't work with it without the addition of some uglycastcalls (as I've added above).smackesey commentedon Mar 10, 2023
Yeah that's close enough-- guess we'll go with casts for now. Thanks!
smackesey commentedon Mar 10, 2023
Hmmm, I've got another related question:
Why the error here? Shouldn't pyright understand that dec_inner returns a function with the same type as it takes in? Or am I missing something about how
ParamSpecworks?erictraut commentedon Mar 10, 2023
ParamSpeccaptures the signature of a function that is assigned to it. In your case, you're passing a TypeVar (T_Callable), so there is no signature to capture. The best a type checker can do is use the signature from the TypeVar's bound, which is.... That means the result of callingdec_inneris(...) -> Any. This isn't compatible with the return typeT_Callablebecause the relationship with the original TypeVar is lost when you pass it through a function.Update pyright 1.1.293 -> 1.1.298 (#12888)