Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Reject legacy TypeVars in PEP 695 functions#25979

Merged
charliermarsh merged 6 commits into
mainfrom
charlie/pep695-legacy-function-typevar
Jun 15, 2026
Merged

[ty] Reject legacy TypeVars in PEP 695 functions#25979
charliermarsh merged 6 commits into
mainfrom
charlie/pep695-legacy-function-typevar

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 14, 2026

Copy link
Copy Markdown
Member

Summary

A function with its own PEP 695 type parameter list cannot also introduce a traditional TypeVar or ParamSpec. Prior to this change, we discovered the legacy generic context while building the signature but discarded it when merging with the PEP 695 context without reporting an error:

from typing import TypeVar

K = TypeVar("K")

def method[M](value: M, other: K) -> M | K:
    raise NotImplementedError

With #25975, we now pass the generics_syntax_compatibility test in the conformance suite.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Jun 14, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 14, 2026

Copy link
Copy Markdown

Typing conformance results improved 🎉

The percentage of diagnostics emitted that were expected errors increased from 94.36% to 94.37%. The percentage of expected errors that received a diagnostic increased from 88.91% to 89.00%. The number of fully passing files improved from 93/134 to 94/134.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 954 955 +1 ⏫ (✅)
False Positives 57 57 +0
False Negatives 119 118 -1 ⏬ (✅)
Total Diagnostics 1059 1060 +1
Precision 94.36% 94.37% +0.01% ⏫ (✅)
Recall 88.91% 89.00% +0.09% ⏫ (✅)
Passing Files 93/134 94/134 +1 ⏫ (✅)

Test file breakdown

1 file altered
File True Positives False Positives False Negatives Status
generics_syntax_compatibility.py 2 (+1) ✅ 0 0 (-1) ✅ ✅ Newly Passing 🎉
Total (all files) 955 (+1) ✅ 57 118 (-1) ✅ 94/134

True positives added (1)

1 diagnostic
Test case Diff

generics_syntax_compatibility.py:26

+error[unbound-type-variable] Legacy type variable `K` cannot be used in a function with PEP 695 type parameters

@astral-sh-bot

astral-sh-bot Bot commented Jun 14, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Jun 14, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Full report with detailed diff (timing results)

@charliermarsh charliermarsh force-pushed the charlie/pep695-legacy-function-typevar branch from 125113e to 749e106 Compare June 14, 2026 16:03
@charliermarsh charliermarsh changed the base branch from main to charlie/fix-pep695-typevar-default-tests June 14, 2026 16:03
@charliermarsh charliermarsh force-pushed the charlie/pep695-legacy-function-typevar branch 2 times, most recently from cac6cc5 to 3b172df Compare June 14, 2026 19:44
@charliermarsh charliermarsh marked this pull request as ready for review June 14, 2026 23:00
@charliermarsh charliermarsh force-pushed the charlie/fix-pep695-typevar-default-tests branch from 4404e35 to 27787ad Compare June 14, 2026 23:02
@charliermarsh charliermarsh force-pushed the charlie/pep695-legacy-function-typevar branch from 3b172df to 3a7276b Compare June 14, 2026 23:06

@AlexWaygood AlexWaygood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, just a few missing edge cases

Comment on lines +52 to +53
def legacy(self, value: V, other: K) -> V | K:
raise NotImplementedError

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I sort-of understand why this is okay according to the letter of the typing spec, but this does seem to be bad style at the very least! Maybe that's for another (opt-in? or warning-level?) diagnostic to complain about, though

Comment on lines +59 to +67
if node.type_params.is_none() {
return;
}

let legacy_default = node
.type_params
.as_deref()
.into_iter()
.flatten()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if node.type_params.is_none() {
return;
}
let legacy_default = node
.type_params
.as_deref()
.into_iter()
.flatten()
let Some(type_params) = node.type_params.as_deref() else {
return;
};
let legacy_default = type_params
.iter()

if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = ty
&& matches!(
typevar.kind(db),
TypeVarKind::Legacy | TypeVarKind::Pep613Alias | TypeVarKind::ParamSpec

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is correct, but it looks wrong, because TypeVarKind::ParamSpec is confusingly named right now. Its name implies that it covers all ParamSpecs, but PEP-695 ParamSpecs are covered by a separate TypeVarKind variant. I suggest that we rename TypeVarKind::ParamSpec to TypeVarKind::LegacyParamSpec in a followup PR to this

Comment on lines +83 to +86
if let Some((typevar, range)) = legacy_default {
report_pep695_function_legacy_typevar(context, typevar, range);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the way the code is structured here means that if a function has multiple invalid defaults in its parameter list, we'll only report the first invalid one. I think we should report multiple diagnostics on this code, for example (one for the default of A, and another for the default of B), but we only report one on your branch currently:

from typing import TypeVar
T = TypeVar("T")

def f[A = T, B = T](x: T) -> T:
    return x

Comment on lines +101 to +107
let Some(typevar) = legacy_context
.variables(db)
.map(|typevar| typevar.typevar(db))
.find(|typevar| !typevar.is_self(db))
else {
return;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there's a similar issue here -- we only report one diagnostic in the following code, but we should report two (one for K and one for L:

from typing import TypeVar

K = TypeVar("K")
L = TypeVar("L")

class C[V]:
    # error: [unbound-type-variable] "Legacy type variable `K` cannot be used in a function with PEP 695 type parameters"
    def mixed[M](self, value: M, other: K, other_other: L) -> M | K | L:
        raise NotImplementedError

Comment on lines +616 to +617
// Invalid mixes retained in the inferred signature are reported during
// post-inference validation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess the other option would be to propagate the error out of this method by returning a Result rather than an Option? Doing it as post-inference validation seems less invasive -- I think the way you've done it is fine

@AlexWaygood AlexWaygood assigned AlexWaygood and unassigned dcreager Jun 15, 2026
charliermarsh added a commit that referenced this pull request Jun 15, 2026
## Summary

Several tests for PEP 695 type-parameter defaults used traditional
`TypeVar`s declared outside the function. Those examples are themselves
invalid because a function cannot combine a PEP 695 type-parameter list
with traditional type variables introduced by the same function.

This updates the existing fixtures to declare both type variables in the
PEP 695 parameter list. The tests continue to cover bound and constraint
compatibility for defaults without also relying on invalid legacy
type-variable usage.

(This is a precursor to #25979,
which starts to flag those invalid usages.)
Base automatically changed from charlie/fix-pep695-typevar-default-tests to main June 15, 2026 13:19
@charliermarsh charliermarsh force-pushed the charlie/pep695-legacy-function-typevar branch from 3a7276b to 5e3aba3 Compare June 15, 2026 13:25
@charliermarsh charliermarsh force-pushed the charlie/pep695-legacy-function-typevar branch from 5e3aba3 to 0ed1dfd Compare June 15, 2026 14:08
@charliermarsh charliermarsh enabled auto-merge (squash) June 15, 2026 14:13
@charliermarsh charliermarsh merged commit 0db3a9e into main Jun 15, 2026
59 checks passed
@charliermarsh charliermarsh deleted the charlie/pep695-legacy-function-typevar branch June 15, 2026 14:14
charliermarsh added a commit that referenced this pull request Jun 15, 2026
## Summary

This is a follow-up to #25979. The `TypeVarKind` variants previously
used the broad names `Legacy`, `Pep695`, and `ParamSpec`, making it
unclear whether a variant represented a `TypeVar` or `ParamSpec` and
which declaration syntax produced it.

This renames them to `LegacyTypeVar`, `Pep695TypeVar`, and
`LegacyParamSpec`, complementing the existing `Pep695ParamSpec` variant.
The names now identify both the parameter kind and declaration style
without changing behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants