Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
97dd83d
Add support for `P.args` and `P.kwargs`
dhruvmanila Nov 14, 2025
c32615a
Avoid raising error when `P` is used in invalid context
dhruvmanila Nov 14, 2025
ddedf20
Small docs tweak
dhruvmanila Nov 15, 2025
e422952
Update `CallableType` to recode as ParamSpec value
dhruvmanila Nov 18, 2025
b6d20ee
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 18, 2025
0f1dddc
Remove debug logs
dhruvmanila Nov 19, 2025
e34532c
Initial attempt to add to generic infra
dhruvmanila Nov 19, 2025
46a45b7
simplify generic code
dhruvmanila Nov 19, 2025
3b7cc3d
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 20, 2025
336edc4
Naively apply type mapping
dhruvmanila Nov 20, 2025
4697fcb
Update mdtest
dhruvmanila Nov 20, 2025
d0b846a
Restrict callable upcast during specialization
dhruvmanila Nov 20, 2025
683e8b7
Consider `(*args: P.args, **kwargs: P.kwargs)` equivalent to `(**P)`
dhruvmanila Nov 20, 2025
87cb422
Fix constraint set to represent `P1 = P2`
dhruvmanila Nov 20, 2025
6270f82
Raise invalid argument type when `P.args` is matched against `P.kwargs`
dhruvmanila Nov 21, 2025
06e3737
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 21, 2025
8af8194
Display paramspec parameter list on single line
dhruvmanila Nov 25, 2025
ecb4b84
Merge two branches of callable in specialization builder
dhruvmanila Nov 25, 2025
1dca873
wip
dhruvmanila Nov 25, 2025
014d5bc
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 25, 2025
31438c0
Fix merge errors
dhruvmanila Nov 25, 2025
c6e247e
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 27, 2025
3680d94
Try using a sub-call to evaluate paramspec
dhruvmanila Nov 27, 2025
a81b550
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 27, 2025
4984259
Support `ParamSpec` in explicit specialization
dhruvmanila Nov 27, 2025
9032d07
Update `paramspec_value` to take parameters instead of signature
dhruvmanila Nov 27, 2025
b37f5ce
Correctly implement the paramspec type / value inference
dhruvmanila Nov 28, 2025
b051197
Avoid replacing paramspec variable itself for default type
dhruvmanila Nov 28, 2025
b11331f
Run pre-commit
dhruvmanila Nov 28, 2025
454ce6f
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 28, 2025
6fd1c21
Fix after merging latest main
dhruvmanila Nov 28, 2025
0f9f117
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 28, 2025
f19ca40
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 29, 2025
c5c2e70
Remove implicit type alias TODO related to ParamSpec
dhruvmanila Nov 29, 2025
2eab441
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Nov 29, 2025
af5f5e7
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 1, 2025
12d2fa4
Fix default specialization for paramspec
dhruvmanila Dec 1, 2025
3056776
Apply type mapping for return type while specializing paramspec
dhruvmanila Dec 1, 2025
f1f79ba
Avoid creating union when `P` is mapped multiple times
dhruvmanila Dec 1, 2025
bf90833
Remove paramspec special casing
dhruvmanila Dec 1, 2025
427966a
Update existing mdtest
dhruvmanila Dec 1, 2025
b5efe3c
Add paramspec test cases
dhruvmanila Dec 1, 2025
e11d8d7
Relation checks for `P.args`/`P.kwargs`
dhruvmanila Dec 1, 2025
a57874c
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 1, 2025
dfeec73
Add more assignability check
dhruvmanila Dec 1, 2025
6c57028
Avoid bound typevar for an unbounded paramspec
dhruvmanila Dec 1, 2025
ed9b4c2
Update mdtest
dhruvmanila Dec 1, 2025
75e18a4
Revert method rename
dhruvmanila Dec 2, 2025
198099a
Fix fuzzer panics
dhruvmanila Dec 2, 2025
106253f
Fix ide tests
dhruvmanila Dec 2, 2025
a67e53f
Display gradual parameters on single line
dhruvmanila Dec 2, 2025
0eeb3b1
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 2, 2025
b358231
Correctly pass around the ParamSpec type variable
dhruvmanila Dec 2, 2025
75c850c
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 3, 2025
0db8bd6
Pass generic context during paramspec specialization
dhruvmanila Dec 3, 2025
80b6a73
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 3, 2025
86b4fe2
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 3, 2025
cb4aa2d
Add upper bound for `P.args` and `P.kwargs`
dhruvmanila Dec 3, 2025
0a1a26d
Allow `ParamSpec` as annotation (revert)
dhruvmanila Dec 3, 2025
ca5ecc8
Allow passing `Any` to specialize a `ParamSpec`
dhruvmanila Dec 3, 2025
4c86300
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 4, 2025
6cfc7b9
Add test for operations on `P.args` / `P.kwargs`
dhruvmanila Dec 4, 2025
774389b
Add tests with overloads
dhruvmanila Dec 4, 2025
9767d9e
Remove leftover code from using the new solver
dhruvmanila Dec 4, 2025
5c87085
Run pre-commit
dhruvmanila Dec 4, 2025
cd2b7a8
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 5, 2025
cffe40f
Address Alex's review comment on mdtests
dhruvmanila Dec 5, 2025
bffdffe
Address Alex's review comments on code changes
dhruvmanila Dec 5, 2025
d81f15d
Apply type mapping for `PartialSpecialization` as well
dhruvmanila Dec 5, 2025
a1749e0
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 5, 2025
c1ef796
Expand documentation
dhruvmanila Dec 5, 2025
423fda2
Add regression test for the cycle
dhruvmanila Dec 5, 2025
edb26d2
Merge branch 'main' into dhruv/paramspec-args-kwargs
dhruvmanila Dec 5, 2025
9a578a3
Add explanation about allowing `Any` to specialize `ParamSpec`
dhruvmanila Dec 5, 2025
a48ac5e
Add test cases around instance attributes and `Final`
dhruvmanila Dec 5, 2025
0dc2492
Use `assert!`
dhruvmanila Dec 5, 2025
7213020
Fix TODO comment
dhruvmanila Dec 5, 2025
18167b1
Run pre-commit
dhruvmanila Dec 5, 2025
e5a4f59
Add TODO about variance
dhruvmanila Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions crates/ty_ide/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2143,15 +2143,13 @@ def function():
"#,
);

// TODO: This should just be `**AB@Alias2 (<variance>)`
// https://github.com/astral-sh/ty/issues/1581
assert_snapshot!(test.hover(), @r"
(
...
) -> tuple[typing.ParamSpec]
(**AB@Alias2) -> tuple[AB@Alias2]
---------------------------------------------
```python
(
...
) -> tuple[typing.ParamSpec]
(**AB@Alias2) -> tuple[AB@Alias2]
```
---------------------------------------------
info[hover]: Hovered content is
Expand Down Expand Up @@ -2292,12 +2290,12 @@ def function():
"#,
);

// TODO: This should be `P@Alias (<variance>)`
// TODO: Should this be constravariant instead?
assert_snapshot!(test.hover(), @r"
typing.ParamSpec
P@Alias (bivariant)
---------------------------------------------
```python
typing.ParamSpec
P@Alias (bivariant)
```
---------------------------------------------
info[hover]: Hovered content is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,10 @@ Using a `ParamSpec` in a `Callable` annotation:
from typing_extensions import Callable

def _[**P1](c: Callable[P1, int]):
# TODO: Should reveal `ParamSpecArgs` and `ParamSpecKwargs`
reveal_type(P1.args) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs)
reveal_type(P1.kwargs) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs)
reveal_type(P1.args) # revealed: P1@_.args
reveal_type(P1.kwargs) # revealed: P1@_.kwargs

# TODO: Signature should be (**P1) -> int
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (**P1@_) -> int
```

And, using the legacy syntax:
Expand All @@ -322,9 +320,8 @@ from typing_extensions import ParamSpec

P2 = ParamSpec("P2")

# TODO: argument list should not be `...` (requires `ParamSpec` support)
def _(c: Callable[P2, int]):
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (**P2@_) -> int
```

## Using `typing.Unpack`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:

def g() -> TypeGuard[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
# TODO: Should reveal a type representing `P.args` and `P.kwargs`
reveal_type(args) # revealed: tuple[@Todo(ParamSpecArgs / ParamSpecKwargs), ...]
reveal_type(kwargs) # revealed: dict[str, @Todo(ParamSpecArgs / ParamSpecKwargs)]
reveal_type(args) # revealed: P@i.args
reveal_type(kwargs) # revealed: P@i.kwargs
return callback(42, *args, **kwargs)

class Foo:
Expand Down Expand Up @@ -65,8 +64,9 @@ def _(
reveal_type(c) # revealed: Unknown
reveal_type(d) # revealed: Unknown

# error: [invalid-type-form] "Variable of type `ParamSpec` is not allowed in a type expression"
def foo(a_: e) -> None:
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`)
reveal_type(a_) # revealed: Unknown
```

## Inheritance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,271 @@ P = ParamSpec("P", default=[A, B])
class A: ...
class B: ...
```

## Validating `ParamSpec` usage

In type annotations, `ParamSpec` is only valid as the first element to `Callable`, the final element
to `Concatenate`, or as a type parameter to `Protocol` or `Generic`.

```py
from typing import ParamSpec, Callable, Concatenate, Protocol, Generic

P = ParamSpec("P")

class ValidProtocol(Protocol[P]):
def method(self, c: Callable[P, int]) -> None: ...

class ValidGeneric(Generic[P]):
def method(self, c: Callable[P, int]) -> None: ...

def valid(
a1: Callable[P, int],
a2: Callable[Concatenate[int, P], int],
) -> None: ...
def invalid(
# TODO: error
a1: P,
# TODO: error
a2: list[P],
# TODO: error
a3: Callable[[P], int],
# TODO: error
a4: Callable[..., P],
# TODO: error
a5: Callable[Concatenate[P, ...], int],
) -> None: ...
```

## Validating `P.args` and `P.kwargs` usage

The components of `ParamSpec` i.e., `P.args` and `P.kwargs` are only valid when used as the
annotated types of `*args` and `**kwargs` respectively.

```py
from typing import Generic, Callable, ParamSpec

P = ParamSpec("P")

def foo1(c: Callable[P, int]) -> None:
def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ...
def nested2(
# error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?"
*args: P.kwargs,
# error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?"
**kwargs: P.args,
) -> None: ...

# TODO: error
def nested3(*args: P.args) -> None: ...

# TODO: error
def nested4(**kwargs: P.kwargs) -> None: ...

# TODO: error
def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ...

# TODO: error
def bar1(*args: P.args, **kwargs: P.kwargs) -> None:
pass

class Foo1:
# TODO: error
def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ...
```

And, they need to be used together.

```py
def foo2(c: Callable[P, int]) -> None:
# TODO: error
def nested1(*args: P.args) -> None: ...

# TODO: error
def nested2(**kwargs: P.kwargs) -> None: ...

class Foo2:
# TODO: error
args: P.args

# TODO: error
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.

```py
class Foo3(Generic[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: ...
```

## Specializing generic classes explicitly

```py
from typing import Any, Generic, ParamSpec, Callable, TypeVar

P1 = ParamSpec("P1")
P2 = ParamSpec("P2")
T1 = TypeVar("T1")

class OnlyParamSpec(Generic[P1]):
attr: Callable[P1, None]

class TwoParamSpec(Generic[P1, P2]):
attr1: Callable[P1, None]
attr2: Callable[P2, None]

class TypeVarAndParamSpec(Generic[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`.

```py
reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None
reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None

def func(c: Callable[P2, None]):
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None

# TODO: error: paramspec is unbound
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None
```

The square brackets can be omitted when `ParamSpec` is the only type variable

```py
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.

```py
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

# TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int`
# TODO: error: paramspec is unbound
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown
```

Nor can they be omitted when there are more than one `ParamSpec`s.

```py
p = TwoParamSpec[[int, str], [int]]()
reveal_type(p.attr1) # revealed: (int, str, /) -> None
reveal_type(p.attr2) # revealed: (int, /) -> None

# error: [invalid-type-arguments]
# error: [invalid-type-arguments]
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]`.

```py
reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int
```

## Specialization when defaults are involved

```toml
[environment]
python-version = "3.13"
```

```py
from typing import Any, Generic, ParamSpec, Callable, TypeVar

P = ParamSpec("P")
PList = ParamSpec("PList", default=[int, str])
PEllipsis = ParamSpec("PEllipsis", default=...)
PAnother = ParamSpec("PAnother", default=P)
PAnotherWithDefault = ParamSpec("PAnotherWithDefault", default=PList)
```

```py
class ParamSpecWithDefault1(Generic[PList]):
attr: Callable[PList, None]

reveal_type(ParamSpecWithDefault1().attr) # revealed: (int, str, /) -> None
reveal_type(ParamSpecWithDefault1[[int]]().attr) # revealed: (int, /) -> None
```

```py
class ParamSpecWithDefault2(Generic[PEllipsis]):
attr: Callable[PEllipsis, None]

reveal_type(ParamSpecWithDefault2().attr) # revealed: (...) -> None
reveal_type(ParamSpecWithDefault2[[int, str]]().attr) # revealed: (int, str, /) -> None
```

```py
class ParamSpecWithDefault3(Generic[P, PAnother]):
attr1: Callable[P, None]
attr2: Callable[PAnother, None]

# `P` hasn't been specialized, so it defaults to `Unknown` 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(Generic[PList, PAnotherWithDefault]):
attr1: Callable[PList, None]
attr2: Callable[PAnotherWithDefault, 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

# TODO: error
# Un-ordered type variables as the default of `PAnother` is `P`
class ParamSpecWithDefault5(Generic[PAnother, P]):
attr: Callable[PAnother, None]

# TODO: error
# PAnother has default as P (another ParamSpec) which is not in scope
class ParamSpecWithDefault6(Generic[PAnother]):
attr: Callable[PAnother, None]
```

## Semantics

The semantics of `ParamSpec` are described in
[the PEP 695 `ParamSpec` document](./../pep695/paramspec.md) to avoid duplication unless there are
any behavior specific to the legacy `ParamSpec` implementation.
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))

# TODO: support `ParamSpec`/`TypeVarTuple` properly
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[]
# TODO: support `TypeVarTuple` properly
# (these should include the `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[P@SingleParamSpec]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec]
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec, P@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleTypeVarTuple))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))

# TODO: support `ParamSpec`/`TypeVarTuple` properly
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[]
# TODO: support `TypeVarTuple` properly
# (these should include the `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[P@SingleParamSpec]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec]
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec, P@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleTypeVarTuple))
Expand Down
Loading
Loading