Alternate draft implementation for PEP 718 -- stricter rules - #21881
Draft
gvanrossum wants to merge 4 commits into
Draft
Alternate draft implementation for PEP 718 -- stricter rules#21881gvanrossum wants to merge 4 commits into
gvanrossum wants to merge 4 commits into
Conversation
for more information, see https://pre-commit.ci
Contributor
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #21337.
This is a variant of #21880 (also vibe-coded by Fable 5) that is stricter about which callables support subscription, and only support scallables that are created using
def.Fable had a lot to say about it:
Fable's review of this change
This is the "strict" horn of Carl's dilemma — subscription only where the checker can prove runtime FunctionType.__getitem__ exists. Mypy has exactly the right evidence for this: CallableType.definition, which is set to the FuncDef when the type came from analyzing an actual def, and — crucially — is preserved through identity-typed decorators ([F: Callable](f: F) -> F returns the same callable type) but absent from types built out of annotations (x: Callable[[int], int]). So the semantics should fall out of a definition check. Let me implement and then map exactly where it draws the line.Ran 14 commands, edited 2 files, edited a memory
Implemented, and the experiment produced a sharp result about what the rule can and can't mean.
The mechanism. The value-subscription path now requires the callable to be statically traceable to a def, using CallableType.definition as evidence. The nice property that motivated this: definition survives identity-typed decorators (which return the argument's own type) but is absent from types built out of annotations. So the line falls almost exactly where you'd want:
Still subscriptable: plain defs, aliases (g = f), bound methods, classmethods, decorated defs.
Now rejected, with a targeted message (Only functions or methods defined with "def" can be subscripted with type arguments): a variable annotated Callable[[T], T], or the result of a function returning a bare generic Callable. Non-generic non-def callables fall through to the ordinary getitem error, unchanged.
Notably, Carl's headline crash cases (functools.partial, lru_cache) were already excluded even under the permissive rule — mypy types those as Instances, which never reach this code path. The strict rule's real bite is on bare-Callable-typed values.
The caveat, which is the interesting finding. definition is error-message metadata: checker.visit_decorator re-attaches it to any CallableType a decorator returns. I probed the consequence: a decorator annotated (...) -> Callable[[T], T] that at runtime returns a callable object rather than a function — the decorated name stays "def-defined" in the checker's eyes, so victim[int] is allowed and would fail at runtime. The identical type produced without decoration (make()[int]) is correctly rejected. So the implementable rule is really "def-defined, where decoration is assumed to preserve def-ness." I don't think a checker can do better without tracking decorator runtime behavior, so if this rule goes in the PEP, that assumption should be stated — and a production implementation would want a dedicated, serialized from_def bit on CallableType rather than piggybacking on definition.
Verification. All 15 unit tests pass (one new strict-mode case), self-check clean, and ~3,700 targeted regression tests pass — the full suite started exceeding this environment's command time limit, so I ran the subsets covering generics, overloads, ParamSpec, functions, classes, inference, and decorators instead; worth one full CI run on your side before treating it as fully clean. Updated patch (427 lines) and the two probe files are above.
One design question worth deciding before this goes to the thread: should the bare-function path also be strict? Currently f[int] via a direct reference is always allowed since semanal proved it's a FuncDef — that's consistent. But an unannotated lambda assigned to a variable is a FunctionType at runtime and would be subscriptable under the PEP's runtime rules, while never generic in practice — I left that case alone.