Skip to content

Fix circular TypeAliasType references in TypedDict and NamedTuple fields - #11815

Open
crypt0lith wants to merge 1 commit into
microsoft:mainfrom
crypt0lith:fix/typealiastype-reentrancy-cycle
Open

crypt0lith wants to merge 1 commit into
microsoft:mainfrom
crypt0lith:fix/typealiastype-reentrancy-cycle

Conversation

@crypt0lith

Copy link
Copy Markdown
Contributor

Summary

Fixes #11810.

Pyright reports a field annotation that refers back to the alias as a variable in a type expression, and discards the declared type of a TypedDict field.

Root cause

Evaluating the value of a TypeAliasType call builds the fields of the TypedDict or NamedTuple classes it refers to, and those fields look up the alias while it is still being evaluated. getTypeOfTypeAliasCommon creates a placeholder for this, but registers it as a partial type only when the alias name has a declaration, and the binder attaches no declaration to the target of an assignment. The lookup then fails the recursion check in getTypeOfSymbolForDecls and produces Unknown. The failed check also invalidates the symbols being resolved above it, including the field whose annotation is being evaluated.

Fix

  • In getTypeOfTypeAliasCommon, find the declaration through the alias symbol when the name node has none.
  • In getTypeOfSymbolForDecls, check for a partial type before the recursion check, as getDeclaredTypeOfSymbol already does.

Test

Adds a test sample typeAliasType3.py that reveals the field types of TypedDict and NamedTuple classes that refer to each other through a TypeAliasType. Without the fix, it reports 3 errors.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Compared candidate 6c2717743bfaca53583abe79aa49c964bb54c1a2 against its first parent fe14a8e051c63f1ea1b76ac08227f73aa923032f.

View the full release history charts with the base and PR results.

Execution time history

Execution time history

Peak memory history

Peak memory history

Download the chart artifact.

Type checker benchmark

🟢 No performance regressions detected.

Regression threshold: 20.0%
Statistic: median
Variance guard: >1.0s time and >100.0 MB memory

Package Checker Files checked Time Time delta Peak memory Memory delta Status
ansible pyright 583 18.139s -0.0% 1107.5 MB +2.2% 🟢 Pass
click pyright 17 2.137s +0.1% 354.7 MB +0.2% 🟢 Pass
homeassistant pyright 9850 123.668s -8.0% 6220.9 MB +2.3% 🟢 Pass
numpy pyright 356 31.813s -5.8% 1293.4 MB -3.3% 🟢 Pass
pandas pyright 1459 101.701s +5.5% 3667.5 MB -7.6% 🟢 Pass
pytest pyright 243 12.803s -0.0% 848.4 MB +0.3% 🟢 Pass
requests pyright 19 2.237s -0.9% 342.2 MB -0.1% 🟢 Pass
torch pyright 2400 136.722s -2.3% 5029.1 MB +6.1% 🟢 Pass
transformers pyright 2901 88.659s +4.1% 5063.2 MB -3.5% 🟢 Pass

Pyright stats

Package Parsed/bound Checked Find Read Tokenize Parse Imports Bind Check Cycles
ansible 989 583 0.040s 0.087s 0.383s 0.737s 0.223s 0.947s 14.900s 0.000s
click 111 17 0.000s 0.010s 0.080s 0.163s 0.027s 0.183s 1.440s 0.000s
homeassistant 11147 9850 0.353s 0.723s 2.787s 4.457s 1.903s 5.873s 107.700s 0.000s
numpy 603 356 0.010s 0.070s 0.353s 0.687s 0.240s 0.800s 29.203s 0.000s
pandas 1895 1459 0.050s 0.197s 0.923s 2.080s 0.250s 2.190s 94.833s 0.000s
pytest 542 243 0.020s 0.057s 0.387s 0.557s 0.113s 0.727s 10.517s 0.000s
requests 168 19 0.000s 0.017s 0.110s 0.203s 0.057s 0.247s 1.293s 0.000s
torch 3079 2400 0.140s 0.387s 2.230s 3.600s 0.567s 4.483s 121.670s 0.000s
transformers 3535 2901 0.077s 0.393s 1.777s 2.733s 0.687s 6.833s 77.627s 0.000s

@heejaechang

Copy link
Copy Markdown
Collaborator

🟢 Pyright CLI QA — ab0c5c67 — verdict: green

Automated CLI checks compared this change with its base revision.
33 focused check(s) completed.

No new behavior difference was detected.

When the value of a TypeAliasType call refers to TypedDict or NamedTuple
classes whose fields refer back to the alias, evaluating the alias
builds those classes' fields, and the fields look up the alias while
it is still being evaluated. getTypeOfTypeAliasCommon creates a
placeholder for this, but never registers it as a partial type for a
TypeAliasType call, because the binder attaches no declaration to the
target of an assignment. The lookup then fails the recursion check in
getTypeOfSymbolForDecls and produces Unknown, which is reported as a
variable in a type expression. The failed check also invalidates the
symbols being resolved above it, which discards the declared type of
a TypedDict field.

Find the declaration through the alias symbol when the name node has
none, and check for a partial type in getTypeOfSymbolForDecls before
the recursion check, as getDeclaredTypeOfSymbol already does.

Adds typeAliasType3.py, which reveals the field types of TypedDict
and NamedTuple classes that refer to each other through a TypeAliasType.

Fixes microsoft#11810.
@crypt0lith
crypt0lith force-pushed the fix/typealiastype-reentrancy-cycle branch from ab0c5c6 to 2fb2b9e Compare November 1, 2026 02:27

This branch was previously deployed

1 inactive (outdated) deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Circular TypeAliasTypes are not understood in TypedDict & NamedTuple

2 participants