Skip to content

[ty] Validate bare ParamSpec usage in type annotations, and support stringified ParamSpecs as the first argument to Callable#23625

Merged
AlexWaygood merged 16 commits intomainfrom
claude/add-paramspec-validation-eOwUW
Mar 5, 2026
Merged

[ty] Validate bare ParamSpec usage in type annotations, and support stringified ParamSpecs as the first argument to Callable#23625
AlexWaygood merged 16 commits intomainfrom
claude/add-paramspec-validation-eOwUW

Conversation

@AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Feb 27, 2026

Summary

Add validation to detect and report errors when a bare ParamSpec is used in invalid contexts. A ParamSpec is only valid:

  • As the first argument to Callable
  • As the last argument to Concatenate
  • As part of a type parameter/argument list (e.g., Generic[P], Protocol[P])

The validation is applied to both PEP 695 style (def foo[**P]) and legacy style (P = ParamSpec("P")) ParamSpec declarations.

While checking whether this validation also worked for stringified ParamSpecs, I realised that we actually didn't support stringified ParamSpecs in some places where we should. So I fixed that as part of this PR, too.

I'm not wild about how we have to "manually" add this check in multiple places, but I don't see an obviously better way of doing this.

^I found an obviously better way of doing this.

Test Plan

mdtests and snapshots

@AlexWaygood AlexWaygood added ty Multi-file analysis & type inference ecosystem-analyzer labels Feb 27, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 27, 2026

Typing conformance results

The percentage of diagnostics emitted that were expected errors decreased from 85.74% to 85.73%. The percentage of expected errors that received a diagnostic increased from 78.13% to 78.45%.

Summary

Metric Old New Diff Outcome
True Positives 854 859 +5 ⏫ (✅)
False Positives 142 143 +1 ⏫ (❌)
False Negatives 239 236 -3 ⏬ (✅)
Total Diagnostics 996 1002 +6
Precision 85.74% 85.73% -0.01% ⏬ (❌)
Recall 78.13% 78.45% +0.31% ⏫ (✅)

True positives added

Details
Location Name Message
generics_paramspec_basic.py:23:14
generics_paramspec_basic.py:23:20
invalid-type-form
invalid-type-form
Bare ParamSpec P is not valid in this context in a type expression
Bare ParamSpec P is not valid in this context in a type expression
generics_paramspec_basic.py:35:35 invalid-type-form Bare ParamSpec P is not valid in this context in a type expression
generics_paramspec_basic.py:39:18
generics_paramspec_basic.py:39:31
invalid-type-form
invalid-type-form
Bare ParamSpec P is not valid in this context in a type expression
Bare ParamSpec P is not valid in this context in a type expression

False positives added

Details
Location Name Message
generics_defaults.py:200:17 invalid-type-form The first argument to Callable must be either a list of types, ParamSpec, Concatenate, or ...

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 27, 2026

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 27, 2026

mypy_primer results

Changes were detected when running on open source projects
beartype (https://github.com/beartype/beartype)
+ beartype/bite/kind/infercallable.py:562:18: error[invalid-type-form] Variable of type `list[Unknown]` is not allowed in a type expression
+ beartype/bite/kind/infercallable.py:562:18: error[invalid-type-form] Variable of type `EllipsisType` is not allowed in a type expression
+ beartype/bite/kind/infercallable.py:562:18: error[invalid-type-form] Variable of type `ParamSpec` is not allowed in a type expression
- Found 500 diagnostics
+ Found 503 diagnostics

mypy (https://github.com/python/mypy)
+ mypy/typeshed/stdlib/typing.pyi:418:53: error[invalid-type-form] Bare ParamSpec `_P` is not valid in this context in a type expression
+ mypy/typeshed/stdlib/typing.pyi:418:74: error[invalid-type-form] Bare ParamSpec `_P` is not valid in this context in a type expression
- Found 1736 diagnostics
+ Found 1738 diagnostics

@AlexWaygood AlexWaygood changed the title [ty] Validate bare ParamSpec usage in type annotations [ty] Validate bare ParamSpec usage in type annotations, and support stringified ParamSpecs as the first argument to Callable Feb 27, 2026
@AlexWaygood AlexWaygood marked this pull request as ready for review February 27, 2026 23:51
@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 28, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-await 40 0 0
invalid-type-form 5 0 0
invalid-return-type 1 0 0
Total 46 0 0

Full report with detailed diff (timing results)

@carljm carljm removed their request for review February 28, 2026 06:33
@AlexWaygood AlexWaygood force-pushed the claude/add-paramspec-validation-eOwUW branch from 7ba9ff6 to 0ffdea6 Compare March 1, 2026 16:14
claude and others added 12 commits March 2, 2026 13:18
Add validation to reject `ParamSpec` usage in positions where it is not
valid. A `ParamSpec` can only be used as the first argument to `Callable`
or the last argument to `Concatenate`. This change detects and reports
`invalid-type-form` diagnostics for the following invalid uses:

- Bare `ParamSpec` as a variable/parameter annotation (e.g., `a: P`)
- `ParamSpec` as a type argument to non-ParamSpec type variables (e.g., `list[P]`)
- `ParamSpec` inside a `Callable` parameter list (e.g., `Callable[[P], int]`)
- `ParamSpec` as the return type of a `Callable` (e.g., `Callable[..., P]`)
- Bare `ParamSpec` as a type alias value (e.g., `type Alias = P`)
- `ParamSpec` in function return type annotations (e.g., `-> P`)

https://claude.ai/code/session_01HAr9mYARjM9pVP8B8JRHtF
- Shorten the primary diagnostic to "A bare `ParamSpec` is not valid in
  this context" and move the detailed guidance to an info subdiagnostic
- Use `raise NotImplementedError` for `invalid_return` test bodies to
  avoid distracting `empty-body` diagnostics
- Add `y: Any` parameter and assign from it to avoid `invalid-assignment`
  diagnostics in `invalid_variable_annotation` tests

https://claude.ai/code/session_01HAr9mYARjM9pVP8B8JRHtF
- Change diagnostic to `Bare ParamSpec `P` is not valid in this context`
  to include the specific ParamSpec name
- Move `Any` import to the top of the code block alongside other
  `typing` imports in the legacy paramspec test

https://claude.ai/code/session_01HAr9mYARjM9pVP8B8JRHtF
…info message

- Remove the `qualifiers.is_empty()` guard so that `ClassVar[P]`,
  `Final[P]`, etc. also trigger the bare ParamSpec diagnostic
- Update the `.info()` message to mention type parameter/argument lists
- Add tests for bare ParamSpec with type qualifiers (e.g. `Final[P]`)
- Add tests for stringified ParamSpecs (e.g. `-> "P"`)
- Update classvar.md to expect the new diagnostic on `ClassVar[P]`

https://claude.ai/code/session_01HAr9mYARjM9pVP8B8JRHtF
Handle `Callable["P", int]` and `Callable[Concatenate[int, "P"], int]`
by parsing the string annotation and recursively processing the inner
expression in `infer_callable_parameter_types`.

https://claude.ai/code/session_01HAr9mYARjM9pVP8B8JRHtF
@AlexWaygood AlexWaygood force-pushed the claude/add-paramspec-validation-eOwUW branch from 8ee930d to a32ae67 Compare March 2, 2026 13:19
Copy link
Contributor

@oconnor663 oconnor663 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My unbridled intellect Claude caught the following cases that aren't currently covered. Worth adding?

def g(_: P | int): ...
def g(_: Union[P, int]): ...
def g(_: Optional[P]): ...

/// `Concatenate`. In all other type expression positions, a bare `ParamSpec` is invalid.
///
/// Returns `true` if the type was a `ParamSpec` and a diagnostic was reported.
pub(crate) fn check_for_bare_paramspec<'db>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "bare" a term of art here? I wonder if it would be clearer to call them "misplaced" ParamSpecs?

@oconnor663 oconnor663 removed their assignment Mar 3, 2026
@AlexWaygood
Copy link
Member Author

My unbridled intellect Claude caught the following cases that aren't currently covered. Worth adding?

I think this actually calls the whole approach into question 😆 I'm going to try a rewrite.

Thank you!!

@AlexWaygood AlexWaygood force-pushed the claude/add-paramspec-validation-eOwUW branch from 5608f33 to 0351ff5 Compare March 4, 2026 21:42
@AlexWaygood AlexWaygood marked this pull request as draft March 4, 2026 21:50
@AlexWaygood AlexWaygood marked this pull request as ready for review March 5, 2026 11:36
@astral-sh-bot astral-sh-bot bot requested a review from carljm March 5, 2026 11:37
Comment on lines 2510 to 2528
#[test]
fn hover_typevar_spec_binding() {
let test = cursor_test(
r#"
from typing import Callable
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
"#,
);

// TODO: This should just be `**AB@Alias2 (<variance>)`
// https://github.com/astral-sh/ty/issues/1581
assert_snapshot!(test.hover(), @"
(**AB@Alias2) -> tuple[AB@Alias2]
(**AB@Alias2) -> tuple[Unknown]
---------------------------------------------
```python
(**AB@Alias2) -> tuple[AB@Alias2]
(**AB@Alias2) -> tuple[Unknown]
```
---------------------------------------------
info[hover]: Hovered content is
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It confused me a bit at first that the snapshot changed here, but it's a good change. AB is a ParamSpec, so it's not a valid type argument to tuple. We now detect that this is invalid, so we infer the value of Alias2 as (**AB@Alias2) -> tuple[Unknown]. We obviously should show **AB@Alias2 instead of (**AB@Alias2) -> tuple[Unknown] when hovering over AB, but that's covered by the existing TODO comment.

@AlexWaygood AlexWaygood force-pushed the claude/add-paramspec-validation-eOwUW branch from 1a911e9 to 394e4b1 Compare March 5, 2026 12:06
@AlexWaygood
Copy link
Member Author

The new false positive in the conformance suite report is due to missing support for TypeVarTuple.

@AlexWaygood AlexWaygood merged commit da13d62 into main Mar 5, 2026
51 checks passed
@AlexWaygood AlexWaygood deleted the claude/add-paramspec-validation-eOwUW branch March 5, 2026 12:26
carljm added a commit that referenced this pull request Mar 5, 2026
* main:
  Update conformance suite commit hash (#23746)
  conformance.py: Collapse the summary paragraph when nothing changed (#23745)
  [ty] Make inferred specializations line up with source types more better (#23715)
  Bump 0.15.5 (#23743)
  [ty] Render all changed diagnostics in conformance.py (#23613)
  [ty] Split deferred checks out of `types/infer/builder.rs` (#23740)
  Discover markdown files by default in preview mode (#23434)
  [ty] Use `HasOptionalDefinition` for `except` handlers (#23739)
  [ty] Fix precedence of `all` selector in TOML configurations (#23723)
  [ty] Make `all` selector case sensitive (#23713)
  [ty] Add a diagnostic if a `TypeVar` is used to specialize a `ParamSpec`, or vice versa (#23738)
  [ty] Override home directory in ty tests (#23724)
  [ty] More type-variable default validation (#23639)
  [ty] Validate bare ParamSpec usage in type annotations, and support stringified ParamSpecs as the first argument to `Callable` (#23625)
  [ty] Add `all` selector to ty.json's `schema` (#23721)
  [ty] Add quotes to related issues links (#23720)
  [ty] Fix panic on incomplete except handlers (#23708)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants