From 5e990b50038313b3823b163c92e5b185d84ae8f1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 14 Nov 2026 19:55:13 -0500 Subject: [PATCH 1/2] [ty] Compare TypedDict body scopes in recursive identity checks When testing whether two active types could have the same recursive identity, compare their static class body scopes. The scope already identifies the class definition; fetching the definition via the semantic index is unnecessary. Synthesized and dynamic TypedDicts use exact identity, which has already been checked. --- crates/ty_python_semantic/src/types/cyclic.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index 801a4e928e1e9..e25dd177eef7e 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -76,7 +76,17 @@ impl<'db> Type<'db> { a.definition(db) == b.definition(db) } (Type::TypeAlias(a), Type::TypeAlias(b)) => a.definition(db) == b.definition(db), - (Type::TypedDict(a), Type::TypedDict(b)) => a.definition(db) == b.definition(db), + ( + Type::TypedDict(TypedDictType::Class(a)), + Type::TypedDict(TypedDictType::Class(b)), + ) => { + // Only a static class can have a GrowingTypedDict identity. Its body scope + // identifies the definition even if decorators changed the class metadata. + matches!( + (a.class_literal(db).as_static(), b.class_literal(db).as_static()), + (Some(a), Some(b)) if a.body_scope(db) == b.body_scope(db) + ) + } (Type::Recursive(a), Type::Recursive(b)) => a.definition(db) == b.definition(db), _ => false, } From 5d9752c4148f2c03d6bafc4f08dfb88aa02482c6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 14 Nov 2026 22:08:48 -0500 Subject: [PATCH 2/2] [ty] Clarify recursive TypedDict identity comment --- crates/ty_python_semantic/src/types/cyclic.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/cyclic.rs b/crates/ty_python_semantic/src/types/cyclic.rs index e25dd177eef7e..1b3a624e8e635 100644 --- a/crates/ty_python_semantic/src/types/cyclic.rs +++ b/crates/ty_python_semantic/src/types/cyclic.rs @@ -80,8 +80,10 @@ impl<'db> Type<'db> { Type::TypedDict(TypedDictType::Class(a)), Type::TypedDict(TypedDictType::Class(b)), ) => { - // Only a static class can have a GrowingTypedDict identity. Its body scope - // identifies the definition even if decorators changed the class metadata. + // Different specializations of the same recursive TypedDict can form a cycle. + // Each class statement has a unique body scope, so comparing scopes tells us + // whether the types came from the same class, even after decorators. + // Other TypedDicts can only match by exact equality, which we checked above. matches!( (a.class_literal(db).as_static(), b.class_literal(db).as_static()), (Some(a), Some(b)) if a.body_scope(db) == b.body_scope(db)