From 0a5235a8ef7a253fd244f8942116eee809f052b9 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 18:12:35 -0800 Subject: [PATCH 1/4] Treat Literal["xyz"] as iterable The extra condition that excluded LiteralType was introduced in #14827 I see no particular reason to have an instance check at all I was looking at this because of this comment from Emma https://github.com/python/mypy/pull/15511#discussion_r1242590017 Previously we errored with `"Literal['xy']" object is not iterable` which is of course totally false Now I issue the same error as in the str case, but restrict to cases where the unpack length does not match --- mypy/checker.py | 19 +++++++++++++------ test-data/unit/check-expressions.test | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index eaaebf17b09b..73860b4811ee 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4117,7 +4117,18 @@ def check_multi_assignment( ) else: if isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": + if rvalue_type.last_known_value is None or ( + isinstance(rvalue_type.last_known_value, str) + and len(rvalue_type.last_known_value) != len(lvalues) + ): + self.msg.unpacking_strings_disallowed(context) + if ( + isinstance(rvalue_type, LiteralType) + and isinstance(rvalue_type.value, str) + and len(lvalues) != len(rvalue_type.value) + ): self.msg.unpacking_strings_disallowed(context) + self.check_multi_assignment_from_iterable( lvalues, rvalue_type, context, infer_lvalue_type ) @@ -4363,9 +4374,7 @@ def check_multi_assignment_from_iterable( infer_lvalue_type: bool = True, ) -> None: rvalue_type = get_proper_type(rvalue_type) - if self.type_is_iterable(rvalue_type) and isinstance( - rvalue_type, (Instance, CallableType, TypeType, Overloaded) - ): + if self.type_is_iterable(rvalue_type): item_type = self.iterable_item_type(rvalue_type, context) for lv in lvalues: if isinstance(lv, StarExpr): @@ -7803,9 +7812,7 @@ def note( return self.msg.note(msg, context, offset=offset, code=code) - def iterable_item_type( - self, it: Instance | CallableType | TypeType | Overloaded, context: Context - ) -> Type: + def iterable_item_type(self, it: ProperType, context: Context) -> Type: if isinstance(it, Instance): iterable = map_instance_to_supertype(it, self.lookup_typeinfo("typing.Iterable")) item_type = iterable.args[0] diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 8d79545ae123..4f67c1fdc659 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2505,3 +2505,24 @@ a2, b2 = s # E: Unpacking a string is disallowed reveal_type(a2) # N: Revealed type is "builtins.str" reveal_type(b2) # N: Revealed type is "builtins.str" [builtins fixtures/primitives.pyi] + +[case testStringLiteralUnpacking] +from typing import Literal, Final + +def takes_literal(xy: Literal["xy"]): + x, y = xy + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(y) # N: Revealed type is "builtins.str" + + x, y, z = xy # E: Unpacking a string is disallowed + reveal_type(z) # N: Revealed type is "builtins.str" + +def last_known_value() -> None: + xy: Final = "xy" + x, y = xy + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(y) # N: Revealed type is "builtins.str" + + x, y, z = xy + reveal_type(z) # N: Revealed type is "builtins.str" +[builtins fixtures/primitives.pyi] From 14016ce99463e2694f24b15b49dfcdd098943ef9 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Mon, 1 Dec 2025 18:22:15 -0800 Subject: [PATCH 2/4] . --- mypy/checker.py | 14 ++++++-------- test-data/unit/check-expressions.test | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 73860b4811ee..6f306a4552e1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4116,16 +4116,14 @@ def check_multi_assignment( lvalues, rvalue, rvalue_type, context, infer_lvalue_type ) else: + rvalue_literal = rvalue_type if isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": - if rvalue_type.last_known_value is None or ( - isinstance(rvalue_type.last_known_value, str) - and len(rvalue_type.last_known_value) != len(lvalues) - ): - self.msg.unpacking_strings_disallowed(context) + if rvalue_type.last_known_value is not None: + rvalue_literal = rvalue_type.last_known_value if ( - isinstance(rvalue_type, LiteralType) - and isinstance(rvalue_type.value, str) - and len(lvalues) != len(rvalue_type.value) + isinstance(rvalue_literal, LiteralType) + and isinstance(rvalue_literal.value, str) + and len(lvalues) != len(rvalue_literal.value) ): self.msg.unpacking_strings_disallowed(context) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 4f67c1fdc659..22854646f934 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2523,6 +2523,6 @@ def last_known_value() -> None: reveal_type(x) # N: Revealed type is "builtins.str" reveal_type(y) # N: Revealed type is "builtins.str" - x, y, z = xy + x, y, z = xy # E: Unpacking a string is disallowed reveal_type(z) # N: Revealed type is "builtins.str" [builtins fixtures/primitives.pyi] From bde3eb7ff5dedffe92a30958552d1156d0fa3e0a Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Mon, 1 Dec 2025 19:00:30 -0800 Subject: [PATCH 3/4] . --- mypy/checker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6f306a4552e1..841b3a905c65 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4118,7 +4118,9 @@ def check_multi_assignment( else: rvalue_literal = rvalue_type if isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": - if rvalue_type.last_known_value is not None: + if rvalue_type.last_known_value is None: + self.msg.unpacking_strings_disallowed(context) + else: rvalue_literal = rvalue_type.last_known_value if ( isinstance(rvalue_literal, LiteralType) From 28b82e26b4930e9248a24708f43ff78e18f29ab1 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Mon, 1 Dec 2025 19:01:23 -0800 Subject: [PATCH 4/4] . --- test-data/unit/check-tuples.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index cfdd2aacc4d2..b4d86b8007e6 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -515,7 +515,7 @@ a, b = None, None # type: (A, B) a1, b1 = a, a # type: (A, B) # E: Incompatible types in assignment (expression has type "A", variable has type "B") a2, b2 = b, b # type: (A, B) # E: Incompatible types in assignment (expression has type "B", variable has type "A") a3, b3 = a # type: (A, B) # E: "A" object is not iterable -a4, b4 = None # type: (A, B) # E: "None" object is not iterable +a4, b4 = None # type: (A, B) # E: "None" has no attribute "__iter__" (not iterable) a5, b5 = a, b, a # type: (A, B) # E: Too many values to unpack (2 expected, 3 provided) ax, bx = a, b # type: (A, B)