crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md
ParamSpecParamSpec was introduced in Python 3.12 while the support for specifying defaults was added in
Python 3.13.
[environment]
python-version = "3.13"
def foo1[**P]() -> None:
reveal_type(P) # revealed: ParamSpec
ParamSpec, when defined using the new syntax, does not allow defining bounds or constraints.
TODO: This results in a lot of syntax errors mainly because the AST doesn't accept them in this position. The parser could do a better job in recovering from these errors.
<!-- fmt:off --># error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
# error: [invalid-syntax]
def foo[**P: int]() -> None:
# error: [invalid-syntax]
# error: [invalid-syntax]
pass
The default value for a ParamSpec can be either a list of types, ..., or another ParamSpec.
def foo2[**P = ...]() -> None:
reveal_type(P) # revealed: ParamSpec
def foo3[**P = [int, str]]() -> None:
reveal_type(P) # revealed: ParamSpec
def foo4[**P, **Q = P]():
reveal_type(P) # revealed: ParamSpec
reveal_type(Q) # revealed: ParamSpec
# error: [invalid-type-form] "Bare ParamSpec `Q` is not valid in this context"
def foo5[**Q, **P = [Q]]() -> None:
pass
Other values are invalid.
# error: [invalid-paramspec]
def foo[**P = int]() -> None:
pass
ParamSpec usageParamSpec is only valid as the first element to Callable or the final element to Concatenate.
from typing import Any, Final, ParamSpec, Callable, Concatenate, Union, Optional, Annotated
def valid[**P](
a1: Callable[P, int],
a2: Callable[Concatenate[int, P], int],
a3: Callable["P", int],
a4: Callable[Concatenate[int, "P"], int],
) -> None: ...
def invalid[**P](
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a1: P,
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a3: Callable[[P], int],
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a4: Callable[..., P],
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a5: Callable[Concatenate[P, ...], int],
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a6: P | int,
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a7: Union[P, int],
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a8: Optional[P],
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a9: Annotated[P, "metadata"],
) -> None: ...
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
def invalid_return[**P]() -> P:
raise NotImplementedError
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
type Alias[**P] = P
def invalid_variable_annotation[**P](y: Any) -> None:
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
x: P = y
def invalid_with_qualifier[**P](y: Any) -> None:
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
x: Final[P] = y
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
def invalid_stringified_return[**P]() -> "P":
raise NotImplementedError
def invalid_stringified_annotation[**P](
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
a: "P",
) -> None: ...
def invalid_stringified_variable_annotation[**P](y: Any) -> None:
# error: [invalid-type-form] "Bare ParamSpec `P` is not valid in this context"
x: "P" = y
class InvalidSpecializationTarget[**P]:
attr: Callable[P, None]
def invalid_specialization[**Q](
# error: [invalid-type-form] "Bare ParamSpec `Q` is not valid in this context"
a: InvalidSpecializationTarget[[Q]],
# error: [invalid-type-form] "Bare ParamSpec `Q` is not valid in this context"
b: InvalidSpecializationTarget[Q,],
) -> None: ...
P.args and P.kwargs usageThe components of ParamSpec i.e., P.args and P.kwargs are only valid when used as the
annotated types of *args and **kwargs respectively.
from typing import Callable
def foo[**P](c: Callable[P, int]) -> None:
def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ...
# error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?"
# error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?"
def nested2(*args: P.kwargs, **kwargs: P.args) -> None: ...
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
def nested3(*args: P.args) -> None: ...
# error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`"
def nested4(**kwargs: P.kwargs) -> None: ...
# error: [invalid-paramspec] "No parameters may appear between `*args: P.args` and `**kwargs: P.kwargs`"
def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ...
# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args`"
def nested6(x: P.args) -> None: ...
def nested7(
*args: P.args,
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
**kwargs: int,
) -> None: ...
And, they need to be used together.
def foo[**P](c: Callable[P, int]) -> None:
# error: [invalid-paramspec] "`*args: P.args` must be accompanied by `**kwargs: P.kwargs`"
def nested1(*args: P.args) -> None: ...
# error: [invalid-paramspec] "`**kwargs: P.kwargs` must be accompanied by `*args: P.args`"
def nested2(**kwargs: P.kwargs) -> None: ...
class Foo[**P]:
# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters"
args: P.args
# error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters"
kwargs: P.kwargs
The name of these parameters does not need to be args or kwargs, it's the annotated type to the
respective variadic parameter that matters.
class Foo3[**P]:
def method1(self, *paramspec_args: P.args, **paramspec_kwargs: P.kwargs) -> None: ...
def method2(
self,
# error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?"
*paramspec_args: P.kwargs,
# error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?"
**paramspec_kwargs: P.args,
) -> None: ...
Error messages for invalid-paramspec also use the actual parameter names:
def bar[**P](c: Callable[P, int]) -> None:
# error: [invalid-paramspec] "`*my_args: P.args` must be accompanied by `**my_kwargs: P.kwargs`"
def f1(*my_args: P.args, **my_kwargs: int) -> None: ...
# error: [invalid-paramspec] "`*positional: P.args` must be accompanied by `**kwargs: P.kwargs`"
def f2(*positional: P.args) -> None: ...
# error: [invalid-paramspec] "`**keyword: P.kwargs` must be accompanied by `*args: P.args`"
def f3(**keyword: P.kwargs) -> None: ...
# error: [invalid-paramspec] "No parameters may appear between `*a: P.args` and `**kw: P.kwargs`"
def f4(*a: P.args, x: int, **kw: P.kwargs) -> None: ...
It isn't allowed to annotate an instance attribute either:
class Foo4[**P]:
def __init__(self, fn: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None:
self.fn = fn
# error: [invalid-paramspec] "`P.args` is only valid for annotating `*args` function parameters"
self.args: P.args = args
# error: [invalid-paramspec] "`P.kwargs` is only valid for annotating `**kwargs` function parameters"
self.kwargs: P.kwargs = kwargs
P.args and P.kwargsThe type of args and kwargs inside the function is P.args and P.kwargs respectively instead
of tuple[P.args, ...] and dict[str, P.kwargs].
*args and **kwargs to a callablefrom typing import Callable
def f[**P](func: Callable[P, int]) -> Callable[P, None]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
reveal_type(args) # revealed: [email protected]
reveal_type(kwargs) # revealed: [email protected]
reveal_type(func(*args, **kwargs)) # revealed: int
# error: [invalid-argument-type] "Argument is incorrect: Expected `[email protected]`, found `[email protected]`"
# error: [invalid-argument-type] "Argument is incorrect: Expected `[email protected]`, found `[email protected]`"
reveal_type(func(*kwargs, **args)) # revealed: int
# error: [invalid-argument-type] "Argument is incorrect: Expected `[email protected]`, found `[email protected]`"
# error: [missing-argument]
reveal_type(func(args, kwargs)) # revealed: int
# Both parameters are required
# error: [missing-argument]
reveal_type(func()) # revealed: int
# error: [missing-argument]
reveal_type(func(*args)) # revealed: int
# error: [missing-argument]
reveal_type(func(**kwargs)) # revealed: int
return wrapper
P.args and P.kwargsThe type of P.args and P.kwargs behave like a tuple and dict respectively. Internally, they
are represented as a type variable that has an upper bound of tuple[object, ...] and
Top[dict[str, Any]] respectively.
from typing import Callable, Any
def f[**P](func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None:
reveal_type(args + ("extra",)) # revealed: tuple[object, ...]
reveal_type(args + (1, 2, 3)) # revealed: tuple[object, ...]
reveal_type(args[0]) # revealed: object
reveal_type("key" in kwargs) # revealed: bool
reveal_type(kwargs.get("key")) # revealed: object
reveal_type(kwargs["key"]) # revealed: object
from typing import Any, Callable, ParamSpec
class OnlyParamSpec[**P1]:
attr: Callable[P1, None]
class TwoParamSpec[**P1, **P2]:
attr1: Callable[P1, None]
attr2: Callable[P2, None]
class TypeVarAndParamSpec[T1, **P1]:
attr: Callable[P1, T1]
Explicit specialization of a generic class involving ParamSpec is done by providing either a list
of types, ..., or another in-scope ParamSpec.
reveal_type(OnlyParamSpec[[]]().attr) # revealed: () -> None
reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None
reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None
def func[**P2](c: Callable[P2, None]):
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None
P2 = ParamSpec("P2")
# error: [invalid-type-arguments] "ParamSpec `P2` is unbound"
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None
# error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`"
reveal_type(OnlyParamSpec[()]().attr) # revealed: (...) -> None
An explicit tuple expression (unlike an implicit one that omits the parentheses) is also accepted
when the ParamSpec is the only type variable. But, this isn't recommended is mainly a fallout of
it having the same AST as the one without the parentheses. Both mypy and Pyright also allow this.
reveal_type(OnlyParamSpec[(int, str)]().attr) # revealed: (int, str, /) -> None
# error: [invalid-syntax]
reveal_type(OnlyParamSpec[]().attr) # revealed: (...) -> None
The square brackets can be omitted when ParamSpec is the only type variable
reveal_type(OnlyParamSpec[int, str]().attr) # revealed: (int, str, /) -> None
reveal_type(OnlyParamSpec[int,]().attr) # revealed: (int, /) -> None
# Even when there is only one element
reveal_type(OnlyParamSpec[Any]().attr) # revealed: (Any, /) -> None
reveal_type(OnlyParamSpec[object]().attr) # revealed: (object, /) -> None
reveal_type(OnlyParamSpec[int]().attr) # revealed: (int, /) -> None
But, they cannot be omitted when there are multiple type variables.
reveal_type(TypeVarAndParamSpec[int, []]().attr) # revealed: () -> int
reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int
reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int
reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "ParamSpec `P2` is unbound"
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> int
Nor can they be omitted when there are more than one ParamSpec.
p = TwoParamSpec[[int, str], [int]]()
reveal_type(p.attr1) # revealed: (int, str, /) -> None
reveal_type(p.attr2) # revealed: (int, /) -> None
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`"
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`"
TwoParamSpec[int, str]
Specializing ParamSpec type variable using typing.Any isn't explicitly allowed by the spec but
both mypy and Pyright allow this and there are usages of this in the wild e.g.,
staticmethod[Any, Any].
reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int
ParamSpec cannot specialize a TypeVar, and vice versaA ParamSpec is not a valid type argument for a regular TypeVar, and vice versa.
from typing import Callable
class OnlyTypeVar[T]:
attr: T
class TypeVarAndParamSpec[T, **P]:
attr: Callable[P, T]
def f[**P, T]():
# error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`"
a: OnlyTypeVar[P]
# error: [invalid-type-arguments] "ParamSpec `P` cannot be used to specialize type variable `T`"
b: TypeVarAndParamSpec[P, [int]]
class OnlyParamSpec[**P]:
attr: Callable[P, None]
# This is fine due to the special case whereby `OnlyParamSpec[T]` is interpreted the same as
# `OnlyParamSpec[[T]]`, due to the fact that `OnlyParamSpec` is only generic over a single
# `ParamSpec` and no other type variables.
def func2[T](c: OnlyParamSpec[T], other: T):
reveal_type(c.attr) # revealed: (T@func2, /) -> None
class ParamSpecAndTypeVar[**P, T]:
attr: Callable[P, T]
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`"
def func3[T](c: ParamSpecAndTypeVar[T, int], other: T): ...
from typing import Callable, ParamSpec
class ParamSpecWithDefault1[**P1 = [int, str]]:
attr: Callable[P1, None]
reveal_type(ParamSpecWithDefault1().attr) # revealed: (int, str, /) -> None
reveal_type(ParamSpecWithDefault1[int]().attr) # revealed: (int, /) -> None
class ParamSpecWithDefault2[**P1 = ...]:
attr: Callable[P1, None]
reveal_type(ParamSpecWithDefault2().attr) # revealed: (...) -> None
reveal_type(ParamSpecWithDefault2[int, str]().attr) # revealed: (int, str, /) -> None
class ParamSpecWithDefault3[**P1, **P2 = P1]:
attr1: Callable[P1, None]
attr2: Callable[P2, None]
# `P1` hasn't been specialized, so it defaults to `...` gradual form
p1 = ParamSpecWithDefault3()
reveal_type(p1.attr1) # revealed: (...) -> None
reveal_type(p1.attr2) # revealed: (...) -> None
p2 = ParamSpecWithDefault3[[int, str]]()
reveal_type(p2.attr1) # revealed: (int, str, /) -> None
reveal_type(p2.attr2) # revealed: (int, str, /) -> None
p3 = ParamSpecWithDefault3[[int], [str]]()
reveal_type(p3.attr1) # revealed: (int, /) -> None
reveal_type(p3.attr2) # revealed: (str, /) -> None
class ParamSpecWithDefault4[**P1 = [int, str], **P2 = P1]:
attr1: Callable[P1, None]
attr2: Callable[P2, None]
p1 = ParamSpecWithDefault4()
reveal_type(p1.attr1) # revealed: (int, str, /) -> None
reveal_type(p1.attr2) # revealed: (int, str, /) -> None
p2 = ParamSpecWithDefault4[[int]]()
reveal_type(p2.attr1) # revealed: (int, /) -> None
reveal_type(p2.attr2) # revealed: (int, /) -> None
p3 = ParamSpecWithDefault4[[int], [str]]()
reveal_type(p3.attr1) # revealed: (int, /) -> None
reveal_type(p3.attr2) # revealed: (str, /) -> None
P2 = ParamSpec("P2")
# error: [invalid-generic-class] "Default of `P1` cannot reference out-of-scope type variable `P2`"
class ParamSpecWithDefault5[**P1 = P2]:
attr: Callable[P1, None]
Most of these test cases are adopted from the
typing documentation on ParamSpec semantics.
ParamSpec oncefrom typing import Callable
def converter[**P](func: Callable[P, int]) -> Callable[P, bool]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool:
func(*args, **kwargs)
return True
return wrapper
def f1(x: int, y: str) -> int:
return 1
# This should preserve all the information about the parameters of `f1`
f2 = converter(f1)
reveal_type(f2) # revealed: (x: int, y: str) -> bool
reveal_type(f1(1, "a")) # revealed: int
reveal_type(f2(1, "a")) # revealed: bool
# As it preserves the parameter kinds, the following should work as well
reveal_type(f2(1, y="a")) # revealed: bool
reveal_type(f2(x=1, y="a")) # revealed: bool
reveal_type(f2(y="a", x=1)) # revealed: bool
# error: [missing-argument] "No argument provided for required parameter `y`"
f2(1)
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["a"]`"
f2("a", "b")
The converter function act as a decorator here:
@converter
def f3(x: int, y: str) -> int:
return 1
reveal_type(f3) # revealed: (x: int, y: str) -> bool
reveal_type(f3(1, "a")) # revealed: bool
reveal_type(f3(x=1, y="a")) # revealed: bool
reveal_type(f3(1, y="a")) # revealed: bool
reveal_type(f3(y="a", x=1)) # revealed: bool
# error: [missing-argument] "No argument provided for required parameter `y`"
f3(1)
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["a"]`"
f3("a", "b")
ParamSpec multiple timesfrom typing import Callable
def multiple[**P](func1: Callable[P, int], func2: Callable[P, int]) -> Callable[P, bool]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool:
func1(*args, **kwargs)
func2(*args, **kwargs)
return True
return wrapper
As per the spec,
A user may include the same
ParamSpecmultiple times in the arguments of the same function, to indicate a dependency between multiple arguments. In these cases a type checker may choose to solve to a common behavioral supertype (i.e. a set of parameters for which all of the valid calls are valid in both of the subtypes), but is not obligated to do so.
TODO: Currently, we don't do this
def xy(x: int, y: str) -> int:
return 1
def yx(y: int, x: str) -> int:
return 2
reveal_type(multiple(xy, xy)) # revealed: (x: int, y: str) -> bool
# The common supertype is `(int, str, /)` which is converting the positional-or-keyword parameters
# into positional-only parameters because the position of the types are the same.
# TODO: This shouldn't error
# error: [invalid-argument-type]
reveal_type(multiple(xy, yx)) # revealed: (x: int, y: str) -> bool
def keyword_only_with_default_1(*, x: int = 42) -> int:
return 1
def keyword_only_with_default_2(*, y: int = 42) -> int:
return 2
# The common supertype for two functions with only keyword-only parameters would be an empty
# parameter list i.e., `()`
# TODO: This shouldn't error
# error: [invalid-argument-type]
# revealed: (*, x: int = 42) -> bool
reveal_type(multiple(keyword_only_with_default_1, keyword_only_with_default_2))
def keyword_only1(*, x: int) -> int:
return 1
def keyword_only2(*, y: int) -> int:
return 2
# On the other hand, combining two functions with only keyword-only parameters does not have a
# common supertype, so it should result in an error.
# error: [invalid-argument-type] "Argument to function `multiple` is incorrect: Expected `(*, x: int) -> int`, found `def keyword_only2(*, y: int) -> int`"
reveal_type(multiple(keyword_only1, keyword_only2)) # revealed: (*, x: int) -> bool
ParamSpecfrom typing import Callable
class C[**P]:
f: Callable[P, int]
def __init__(self, f: Callable[P, int]) -> None:
self.f = f
# Note that the return type must match exactly, since C is invariant on the return type of C.f.
def f(x: int, y: str) -> int:
return True
c = C(f)
reveal_type(c.f) # revealed: (x: int, y: str) -> int
ParamSpec in prepended positional parametersIf one of these prepended positional parameters contains a free
ParamSpec, we consider that variable in scope for the purposes of extracting the components of thatParamSpec.
from typing import Callable
def foo1[**P1](func: Callable[P1, int], *args: P1.args, **kwargs: P1.kwargs) -> int:
return func(*args, **kwargs)
def foo1_with_extra_arg[**P1](func: Callable[P1, int], extra: str, *args: P1.args, **kwargs: P1.kwargs) -> int:
return func(*args, **kwargs)
def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> None:
foo1(func, *args, **kwargs)
# error: [invalid-argument-type] "Argument to function `foo1` is incorrect: Expected `[email protected]`, found `Literal[1]`"
foo1(func, 1, *args, **kwargs)
# error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `str`, found `[email protected]`"
foo1_with_extra_arg(func, *args, **kwargs)
foo1_with_extra_arg(func, "extra", *args, **kwargs)
Here, the first argument to f can specialize P to the parameters of the callable passed to it
which is then used to type the ParamSpec components used in *args and **kwargs.
def f1(x: int, y: str) -> int:
return 1
foo1(f1, 1, "a")
foo1(f1, x=1, y="a")
foo1(f1, 1, y="a")
# error: [missing-argument] "No arguments provided for required parameters `x`, `y` of function `foo1`"
foo1(f1)
# error: [missing-argument] "No argument provided for required parameter `y` of function `foo1`"
foo1(f1, 1)
# error: [invalid-argument-type] "Argument to function `foo1` is incorrect: Expected `str`, found `Literal[2]`"
foo1(f1, 1, 2)
# error: [too-many-positional-arguments] "Too many positional arguments to function `foo1`: expected 2, got 3"
foo1(f1, 1, "a", "b")
# error: [missing-argument] "No argument provided for required parameter `y` of function `foo1`"
# error: [unknown-argument] "Argument `z` does not match any known parameter of function `foo1`"
foo1(f1, x=1, z="a")
ParamSpec with another ParamSpecclass Foo[**P]:
def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None:
self.args = args
self.kwargs = kwargs
def bar[**P](foo: Foo[P]) -> None:
reveal_type(foo) # revealed: Foo[P@bar]
reveal_type(foo.args) # revealed: [email protected]
reveal_type(foo.kwargs) # revealed: [email protected]
ty will check whether the argument after ** is a mapping type, but the inferred attribute type
preserves the parameter pack here, so it shouldn't error.
from typing import Callable
def baz[**P](fn: Callable[P, None], foo: Foo[P]) -> None:
fn(*foo.args, **foo.kwargs)
The Unknown can be eliminated by using annotating these attributes with Final:
from typing import Final
class FooWithFinal[**P]:
def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None:
self.args: Final = args
self.kwargs: Final = kwargs
def with_final[**P](foo: FooWithFinal[P]) -> None:
reveal_type(foo) # revealed: FooWithFinal[P@with_final]
reveal_type(foo.args) # revealed: P@with_final.args
reveal_type(foo.kwargs) # revealed: P@with_final.kwargs
Self when ParamSpec is involvedclass Foo[**P]:
def method(self, *args: P.args, **kwargs: P.kwargs) -> str:
return "hello"
foo = Foo[int, str]()
reveal_type(foo) # revealed: Foo[(int, str, /)]
reveal_type(foo.method) # revealed: bound method Foo[(int, str, /)].method(int, str, /) -> str
reveal_type(foo.method(1, "a")) # revealed: str
ParamSpec inferencefrom typing import Callable
def callable_identity[**P, R](func: Callable[P, R]) -> Callable[P, R]:
return func
@callable_identity
def f(env: dict) -> None:
pass
# revealed: (env: dict[Unknown, Unknown]) -> None
reveal_type(f)
overloaded.pyi:
from typing import overload
@overload
def int_int(x: int) -> int: ...
@overload
def int_int(x: str) -> int: ...
@overload
def int_str(x: int) -> int: ...
@overload
def int_str(x: str) -> str: ...
@overload
def str_str(x: int) -> str: ...
@overload
def str_str(x: str) -> str: ...
from typing import Callable
from overloaded import int_int, int_str, str_str
def change_return_type[**P](f: Callable[P, int]) -> Callable[P, str]:
def nested(*args: P.args, **kwargs: P.kwargs) -> str:
return str(f(*args, **kwargs))
return nested
def with_parameters[**P](f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> Callable[P, str]:
def nested(*args: P.args, **kwargs: P.kwargs) -> str:
return str(f(*args, **kwargs))
return nested
reveal_type(change_return_type(int_int)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# TODO: This shouldn't error and should pick the first overload because of the return type
# error: [invalid-argument-type]
reveal_type(change_return_type(int_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# error: [invalid-argument-type]
reveal_type(change_return_type(str_str)) # revealed: (...) -> str
# TODO: This should reveal the matching overload instead
reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# error: [invalid-argument-type] "Argument to function `with_parameters` is incorrect: Expected `int`, found `None`"
reveal_type(with_parameters(int_int, None)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
def foo(int_or_str: int | str):
# Argument type expansion leads to matching both overloads.
# TODO: Should this be an error instead?
reveal_type(with_parameters(int_int, int_or_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# Keyword argument matching should also work
# TODO: This should reveal the matching overload instead
reveal_type(with_parameters(int_int, x=1)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
reveal_type(with_parameters(int_int, x="a")) # revealed: Overload[(x: int) -> str, (x: str) -> str]
# No matching overload should error
# error: [invalid-argument-type]
reveal_type(with_parameters(int_int, 1.5)) # revealed: Overload[(x: int) -> str, (x: str) -> str]
overloaded.pyi:
from typing import overload
@overload
def multi(x: int, y: int) -> int: ...
@overload
def multi(x: str, y: str) -> str: ...
from typing import Callable
from overloaded import multi
def run[**P, R](f: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:
return f(*args, **kwargs)
# Both arguments match first overload
# TODO: should reveal `int`
reveal_type(run(multi, 1, 2)) # revealed: int | str
# Both arguments match second overload
# TODO: should reveal `str`
reveal_type(run(multi, "a", "b")) # revealed: int | str
# Mixed positional and keyword
# TODO: both should reveal `int`
reveal_type(run(multi, 1, y=2)) # revealed: int | str
reveal_type(run(multi, x=1, y=2)) # revealed: int | str
# No matching overload (int, str doesn't match either overload of `multi`)
# error: [invalid-argument-type]
reveal_type(run(multi, 1, "b")) # revealed: int | str
P.args and P.kwargsThis is regression test for https://github.com/astral-sh/ty/issues/2027
from typing import Callable, Never, overload
class Task[**P, R]:
def __init__(self, func: Callable[P, R]) -> None:
self.func = func
@overload
def __call__(self: "Task[P, R]", *args: P.args, **kwargs: P.kwargs) -> R: ...
@overload
def __call__(self: "Task[P, Never]", *args: P.args, **kwargs: P.kwargs) -> None: ...
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R | None:
return self.func(*args, **kwargs)
def returns_str(x: int) -> str:
return str(x)
def never_returns(x: int) -> Never:
raise Exception()
t1 = Task(returns_str)
reveal_type(t1) # revealed: Task[(x: int), str]
reveal_type(t1(1)) # revealed: str
reveal_type(t1(x=1)) # revealed: str
# error: [no-matching-overload]
reveal_type(t1("a")) # revealed: Unknown
# error: [no-matching-overload]
reveal_type(t1(y=1)) # revealed: Unknown
t2 = Task(never_returns)
# TODO: This should be `Task[(x: int), Never]`
reveal_type(t2) # revealed: Task[(x: int), Unknown]
# TODO: This should be `Never`
reveal_type(t2(1)) # revealed: Unknown
When comparing signatures with ParamSpec attributes (P.args and P.kwargs), two different
inferable ParamSpec attributes with the same kind are assignable to each other. This enables
method overrides where both methods have their own ParamSpec.
from typing import Callable
class Parent:
def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]:
return callback
class Child1(Parent):
# This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
return callback
# Both signatures use ParamSpec, so they should be compatible
def outer[**P](f: Callable[P, int]) -> Callable[P, int]:
def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]:
return g
return inner(f)
We can explicitly mark it as an override using the @override decorator.
from typing import override
class Child2(Parent):
@override
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
return callback
ParamSpec not inferableHere, P is in a non-inferable position while Q is inferable. So, they are not considered
assignable.
from typing import Callable
class Container[**P]:
def method(self, f: Callable[P, None]) -> Callable[P, None]:
return f
def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]:
# error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`"
# error: [invalid-argument-type] "Argument to bound method `Container.method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
return self.method(f)
ParamSpec inference with un-annotated return typeRegression test for an issue where ParamSpec inference failed when the callable we were inferring
from did not have an annotated return type.
from typing import Callable
def infer_paramspec[**P](func: Callable[P, None]) -> Callable[P, None]:
return func
def f(x: int, y: str):
pass
reveal_type(infer_paramspec(f)) # revealed: (x: int, y: str) -> None
ParamSpec decoratorsWhen a generic function is decorated with a ParamSpec-based decorator, the generic context of the
decorated function should be preserved. This allows type inference to work correctly when calling
the decorated function.
Regression test for https://github.com/astral-sh/ty/issues/2336
from typing import Callable
from ty_extensions import generic_context
def decorator[**P, T](func: Callable[P, T]) -> Callable[P, T]:
return func
@decorator
def identity[T](value: T) -> T:
return value
@decorator
def pair[T, U](first: T, second: U) -> tuple[T, U]:
return (first, second)
# revealed: ty_extensions.GenericContext[T@identity]
reveal_type(generic_context(identity))
# revealed: ty_extensions.GenericContext[T@pair, U@pair]
reveal_type(generic_context(pair))
reveal_type(identity(1)) # revealed: Literal[1]
reveal_type(identity("hello")) # revealed: Literal["hello"]
reveal_type(pair(1, "a")) # revealed: tuple[Literal[1], Literal["a"]]
reveal_type(pair("x", 2.5)) # revealed: tuple[Literal["x"], float]
from typing import Callable
def decorator1[**P, R](func: Callable[P, R]) -> Callable[P, R]:
return func
def decorator2[**P, R](func: Callable[P, R]) -> Callable[P, R]:
return func
@decorator1
@decorator2
def chained_generic[T](value: T) -> T:
return value
reveal_type(chained_generic(42)) # revealed: Literal[42]
reveal_type(chained_generic("test")) # revealed: Literal["test"]
from typing import Callable
from ty_extensions import generic_context
def method_decorator[**P, R](func: Callable[P, R]) -> Callable[P, R]:
return func
class Container:
@method_decorator
def generic_method[T](self, value: T) -> T:
return value
c = Container()
# revealed: ty_extensions.GenericContext[T@generic_method]
reveal_type(generic_context(c.generic_method))
reveal_type(c.generic_method) # revealed: [T](value: T) -> T
reveal_type(c.generic_method(100)) # revealed: Literal[100]
reveal_type(c.generic_method([1, 2, 3])) # revealed: list[int]
ParamSpec and class constructorsWhen a class is passed to a function expecting a callable protocol with ParamSpec, the ParamSpec
should be inferred from the class's constructor signature. This inferred signature must then be used
to validate any additional arguments that use the ParamSpec components.
from typing import Protocol
class ParentClass: ...
class MyProto[**P](Protocol):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> ParentClass: ...
class MyType(ParentClass):
def __init__(self, x: int):
pass
def create[**P](p: MyProto[P], *args: P.args, **kwargs: P.kwargs) -> ParentClass:
return p(*args, **kwargs)
# When MyType is passed, P should be inferred as [x: int] from MyType's __init__.
# Since create() requires *args: P.args and **kwargs: P.kwargs, and P is [x: int],
# we must provide the `x` argument.
# error: [missing-argument] "No argument provided for required parameter `x`"
create(MyType)
# These should work since we're providing the required argument
create(MyType, 1)
create(MyType, x=1)
# error: [invalid-argument-type]
create(MyType, "wrong type")
A class with no required constructor parameters should work without additional arguments:
class NoArgs(ParentClass):
pass
create(NoArgs) # OK - P is inferred as []
Multiple parameters:
class MultiParam(ParentClass):
def __init__(self, x: int, y: str):
pass
# error: [missing-argument]
create(MultiParam)
# error: [missing-argument]
create(MultiParam, 1)
create(MultiParam, 1, "hello")
create(MultiParam, x=1, y="hello")
create(MultiParam, 1, y="hello")
# error: [too-many-positional-arguments]
create(MultiParam, 1, "hello", "extra")
# error: [invalid-argument-type]
create(MultiParam, "wrong", "hello")
Optional parameters (default values):
class WithDefaults(ParentClass):
def __init__(self, x: int, y: str = "default"):
pass
# error: [missing-argument]
create(WithDefaults)
# OK - y has a default
create(WithDefaults, 1)
create(WithDefaults, 1, "custom")
create(WithDefaults, x=1)
create(WithDefaults, x=1, y="custom")
Keyword-only parameters:
class KeywordOnly(ParentClass):
def __init__(self, *, x: int):
pass
# error: [missing-argument]
create(KeywordOnly)
# Passing positional where keyword-only is expected
# error: [missing-argument]
# error: [too-many-positional-arguments]
create(KeywordOnly, 1)
create(KeywordOnly, x=1)
Positional-only parameters:
class PositionalOnly(ParentClass):
def __init__(self, x: int, /):
pass
# error: [missing-argument]
create(PositionalOnly)
create(PositionalOnly, 1)
# error: [positional-only-parameter-as-kwarg]
create(PositionalOnly, x=1)
The protocol requires the return type to be ParentClass, so passing a class that doesn't inherit
from ParentClass should produce an error:
class Unrelated:
def __init__(self, x: int):
pass
# error: [invalid-argument-type]
create(Unrelated, 1)
When the protocol has parameters before the ParamSpec (i.e., Concatenate-style signatures), the
callable should still match correctly and P should be inferred as empty:
def my_factory(arg: str) -> int:
return 0
class Factory[**P](Protocol):
def __call__(self, arg: str, *args: P.args, **kwargs: P.kwargs) -> int: ...
def call_factory[**P](ctr: Factory[P], *args: P.args, **kwargs: P.kwargs) -> int:
return ctr("", *args, **kwargs)
call_factory(my_factory)