From 9e64ee623d20eaafffa7a89ea26141c12b15d17c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Mar 2026 10:19:16 -0400 Subject: [PATCH 1/2] [ty] Raise diagnostic for functional TypedDict with non-literal name --- .../resources/mdtest/typed_dict.md | 11 +++++++ .../src/types/infer/builder/typed_dict.rs | 31 +++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index e8c64705a7747..7a7de94f98bd4 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -2535,6 +2535,17 @@ Bad1 = TypedDict(123, {"name": str}) # error: [invalid-argument-type] "The name of a `TypedDict` (`WrongName`) must match the name of the variable it is assigned to (`BadTypedDict3`)" BadTypedDict3 = TypedDict("WrongName", {"name": str}) +def f(x: str) -> None: + # error: [invalid-argument-type] "The first argument to `TypedDict` must be the string literal `Y`" + Y = TypedDict(x, {}) + +def g(x: str) -> None: + # error: [invalid-argument-type] "The first argument to `TypedDict` must be a string literal" + TypedDict(x, {}) + +name = "GoodTypedDict" +GoodTypedDict = TypedDict(name, {"name": str}) + # error: [invalid-argument-type] "Expected a dict literal for parameter `fields` of `TypedDict()`" Bad2 = TypedDict("Bad2", "not a dict") diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index 2928633a22b35..c22fd8367ef65 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -173,16 +173,27 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Name::new(name) } else { - if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) - { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `typename` of `TypedDict()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `str`, found `{}`", - name_type.display(db) - )); + let is_str = name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)); + if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { + if let Some(assigned_name) = definition.and_then(|definition| definition.name(db)) + && is_str + { + builder.into_diagnostic(format_args!( + "The first argument to `TypedDict` must be the string literal `{assigned_name}`" + )); + } else if is_str { + builder.into_diagnostic( + "The first argument to `TypedDict` must be a string literal", + ); + } else { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `typename` of `TypedDict()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `str`, found `{}`", + name_type.display(db) + )); + } } Name::new_static("") }; From 35b633cd54bcd2f876dd34613019856a37312c11 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Mar 2026 13:03:53 -0400 Subject: [PATCH 2/2] Review feedback --- .../resources/mdtest/typed_dict.md | 9 ++- .../src/types/infer/builder/typed_dict.rs | 67 +++++++++---------- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/typed_dict.md b/crates/ty_python_semantic/resources/mdtest/typed_dict.md index 7a7de94f98bd4..ff0b5dce9af59 100644 --- a/crates/ty_python_semantic/resources/mdtest/typed_dict.md +++ b/crates/ty_python_semantic/resources/mdtest/typed_dict.md @@ -2529,19 +2529,18 @@ Movie2 = TypedDict("Movie2", name=str, year=int) ```py from typing_extensions import TypedDict -# error: [invalid-argument-type] "Invalid argument to parameter `typename` of `TypedDict()`" +# error: [invalid-argument-type] "TypedDict name must match the variable it is assigned to: Expected "Bad1", got variable of type `Literal[123]`" Bad1 = TypedDict(123, {"name": str}) -# error: [invalid-argument-type] "The name of a `TypedDict` (`WrongName`) must match the name of the variable it is assigned to (`BadTypedDict3`)" +# error: [invalid-argument-type] "TypedDict name must match the variable it is assigned to: Expected "BadTypedDict3", got "WrongName"" BadTypedDict3 = TypedDict("WrongName", {"name": str}) def f(x: str) -> None: - # error: [invalid-argument-type] "The first argument to `TypedDict` must be the string literal `Y`" + # error: [invalid-argument-type] "TypedDict name must match the variable it is assigned to: Expected "Y", got variable of type `str`" Y = TypedDict(x, {}) def g(x: str) -> None: - # error: [invalid-argument-type] "The first argument to `TypedDict` must be a string literal" - TypedDict(x, {}) + TypedDict(x, {}) # fine name = "GoodTypedDict" GoodTypedDict = TypedDict(name, {"name": str}) diff --git a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs index c22fd8367ef65..21a261ccc1da5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typed_dict.rs @@ -158,45 +158,40 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ); } - let name = if let Some(literal) = name_type.as_string_literal() { - let name = literal.value(db); - - if let Some(assigned_name) = definition.and_then(|definition| definition.name(db)) - && name != assigned_name - && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) - { - builder.into_diagnostic(format_args!( - "The name of a `TypedDict` (`{name}`) must match \ - the name of the variable it is assigned to (`{assigned_name}`)" + let name = name_type + .as_string_literal() + .map(|literal| Name::new(literal.value(db))); + + if let Some(definition) = definition + && let Some(assigned_name) = definition.name(db) + && Some(assigned_name.as_str()) != name.as_deref() + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) + { + let mut diagnostic = + builder.into_diagnostic("TypedDict name must match the variable it is assigned to"); + if let Some(name) = name.as_deref() { + diagnostic.set_primary_message(format_args!( + "Expected \"{assigned_name}\", got \"{name}\"" + )); + } else { + diagnostic.set_primary_message(format_args!( + "Expected \"{assigned_name}\", got variable of type `{}`", + name_type.display(db) )); } + } else if !name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)) + && let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) + { + let mut diagnostic = builder.into_diagnostic(format_args!( + "Invalid argument to parameter `typename` of `TypedDict()`" + )); + diagnostic.set_primary_message(format_args!( + "Expected `str`, found `{}`", + name_type.display(db) + )); + } - Name::new(name) - } else { - let is_str = name_type.is_assignable_to(db, KnownClass::Str.to_instance(db)); - if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, name_arg) { - if let Some(assigned_name) = definition.and_then(|definition| definition.name(db)) - && is_str - { - builder.into_diagnostic(format_args!( - "The first argument to `TypedDict` must be the string literal `{assigned_name}`" - )); - } else if is_str { - builder.into_diagnostic( - "The first argument to `TypedDict` must be a string literal", - ); - } else { - let mut diagnostic = builder.into_diagnostic(format_args!( - "Invalid argument to parameter `typename` of `TypedDict()`" - )); - diagnostic.set_primary_message(format_args!( - "Expected `str`, found `{}`", - name_type.display(db) - )); - } - } - Name::new_static("") - }; + let name = name.unwrap_or_else(|| Name::new_static("")); if let Some(definition) = definition { self.deferred.insert(definition);