diff --git a/pyrefly/lib/alt/class/class_field.rs b/pyrefly/lib/alt/class/class_field.rs index 224580cb70..bd573f53e9 100644 --- a/pyrefly/lib/alt/class/class_field.rs +++ b/pyrefly/lib/alt/class/class_field.rs @@ -1560,12 +1560,20 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { self.determine_read_only_reason(name, annotation.as_ref(), &metadata, field_definition); // Determine the final type, promoting literals when appropriate. + let mut has_literal = false; + if matches!(initialization, ClassFieldInitialization::Method) { + value_ty.universe(&mut |current_type_node| { + if matches!(current_type_node, Type::Literal(_) | Type::LiteralString) { + has_literal = true; + } + }); + } let ty = if annotation .as_ref() .and_then(|ann| ann.ty.as_ref()) .is_none() && matches!(read_only_reason, None | Some(ReadOnlyReason::NamedTuple)) - && value_ty.is_literal() + && (value_ty.is_literal() || has_literal) { value_ty.promote_literals(self.stdlib) } else { diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index 3bbfb999de..16d244fc3e 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -90,6 +90,19 @@ class A: "#, ); +testcase!( + test_unannotated_attribute_tuple_literal_promotion, + r#" +from typing import assert_type +class A: + def __init__(self): + self.x = (42, 42) +def f(a: A): + assert_type(a.x, tuple[int, int]) + a.x = (0, 0) + "#, +); + testcase!( test_super_object_bad_assignment, r#"