From ea7323771d478374cbe011cf22af8ce7ab013a17 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 16:41:58 -0800 Subject: [PATCH 1/4] error code --- mypy/errorcodes.py | 3 +++ mypy/messages.py | 2 +- test-data/unit/check-expressions.test | 17 +++++++++++++++++ test-data/unit/check-unions.test | 14 -------------- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 3fadd7bc07e7..927cd32f8fe0 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -215,6 +215,9 @@ def __hash__(self) -> int: "General", default_enabled=False, ) +STR_UNPACK: Final[ErrorCode] = ErrorCode( + "str-unpack", "Warn about expressions that unpack str", "General" +) NAME_MATCH: Final = ErrorCode( "name-match", "Check that type definition has consistent naming", "General" ) diff --git a/mypy/messages.py b/mypy/messages.py index 9fdfb748b288..4945a5a7c45b 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1144,7 +1144,7 @@ def wrong_number_values_to_unpack( ) def unpacking_strings_disallowed(self, context: Context) -> None: - self.fail("Unpacking a string is disallowed", context) + self.fail("Unpacking a string is disallowed", context, code=codes.STR_UNPACK) def type_not_iterable(self, type: Type, context: Context) -> None: self.fail(f"{format_type(type, self.options)} object is not iterable", context) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index ea6eac9a39b3..18e28fa9f2a6 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2492,3 +2492,20 @@ x + T # E: Unsupported left operand type for + ("int") T() # E: "TypeVar" not callable [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] + +[case testStringDisallowedUnpacking] +d: dict[str, str] + +for a1, b1 in d: # E: Unpacking a string is disallowed + reveal_type(a1) # E: Cannot determine type of "a1" \ + # N: Revealed type is "Any" + reveal_type(b1) # E: Cannot determine type of "b1" \ + # N: Revealed type is "Any" + +s = "foo" +a2, b2 = s # E: Unpacking a string is disallowed +reveal_type(a2) # E: Cannot determine type of "a2" \ + # N: Revealed type is "Any" +reveal_type(b2) # E: Cannot determine type of "b2" \ + # N: Revealed type is "Any" +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index 398b007ce57d..b87b84dc8411 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -729,20 +729,6 @@ reveal_type(x) # N: Revealed type is "Any" reveal_type(y) # N: Revealed type is "Any" [out] -[case testStringDisallowedUnpacking] -from typing import Dict - -d: Dict[str, str] - -for a, b in d: # E: Unpacking a string is disallowed - pass - -s = "foo" -a, b = s # E: Unpacking a string is disallowed - -[builtins fixtures/dict.pyi] -[out] - [case testUnionAlwaysTooMany] from typing import Union, Tuple bad: Union[Tuple[int, int, int], Tuple[str, str, str]] From e3063bf1fd08cb6cac0649c57480ef6e87667a08 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 16:42:34 -0800 Subject: [PATCH 2/4] preserve types --- mypy/checker.py | 4 ++-- test-data/unit/check-expressions.test | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 461b45f8df45..26915103a279 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4106,9 +4106,9 @@ def check_multi_assignment( self.check_multi_assignment_from_union( lvalues, rvalue, rvalue_type, context, infer_lvalue_type ) - elif isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": - self.msg.unpacking_strings_disallowed(context) else: + if isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": + self.msg.unpacking_strings_disallowed(context) self.check_multi_assignment_from_iterable( lvalues, rvalue_type, context, infer_lvalue_type ) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 18e28fa9f2a6..bc2b08bfd440 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2496,14 +2496,16 @@ T() # E: "TypeVar" not callable [case testStringDisallowedUnpacking] d: dict[str, str] -for a1, b1 in d: # E: Unpacking a string is disallowed +for a1, b1 in d: # E: Unpacking a string is disallowed \ + # E: "str" object is not iterable reveal_type(a1) # E: Cannot determine type of "a1" \ # N: Revealed type is "Any" reveal_type(b1) # E: Cannot determine type of "b1" \ # N: Revealed type is "Any" s = "foo" -a2, b2 = s # E: Unpacking a string is disallowed +a2, b2 = s # E: Unpacking a string is disallowed \ + # E: "str" object is not iterable reveal_type(a2) # E: Cannot determine type of "a2" \ # N: Revealed type is "Any" reveal_type(b2) # E: Cannot determine type of "b2" \ From 778f8b7a640975af280430522cc30ecad7079da9 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 17:14:16 -0800 Subject: [PATCH 3/4] fix fixtures --- test-data/unit/check-expressions.test | 20 +++++++------------- test-data/unit/check-unions.test | 7 ++++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index bc2b08bfd440..8d79545ae123 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2496,18 +2496,12 @@ T() # E: "TypeVar" not callable [case testStringDisallowedUnpacking] d: dict[str, str] -for a1, b1 in d: # E: Unpacking a string is disallowed \ - # E: "str" object is not iterable - reveal_type(a1) # E: Cannot determine type of "a1" \ - # N: Revealed type is "Any" - reveal_type(b1) # E: Cannot determine type of "b1" \ - # N: Revealed type is "Any" +for a1, b1 in d: # E: Unpacking a string is disallowed + reveal_type(a1) # N: Revealed type is "builtins.str" + reveal_type(b1) # N: Revealed type is "builtins.str" s = "foo" -a2, b2 = s # E: Unpacking a string is disallowed \ - # E: "str" object is not iterable -reveal_type(a2) # E: Cannot determine type of "a2" \ - # N: Revealed type is "Any" -reveal_type(b2) # E: Cannot determine type of "b2" \ - # N: Revealed type is "Any" -[builtins fixtures/dict.pyi] +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] diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index b87b84dc8411..a578735ec2c5 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -725,9 +725,10 @@ bad: Union[int, str] x, y = bad # E: "int" object is not iterable \ # E: Unpacking a string is disallowed -reveal_type(x) # N: Revealed type is "Any" -reveal_type(y) # N: Revealed type is "Any" -[out] +reveal_type(x) # N: Revealed type is "Union[Any, builtins.str]" +reveal_type(y) # N: Revealed type is "Union[Any, builtins.str]" +[builtins fixtures/primitives.pyi] + [case testUnionAlwaysTooMany] from typing import Union, Tuple From e6800110116f06cbe4428e5c12ef1ab0eed36526 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 17:17:52 -0800 Subject: [PATCH 4/4] add docs --- docs/source/error_code_list.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 1aec6cf7dbde..034c795e7120 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -1151,6 +1151,25 @@ Warn about cases where a bytes object may be converted to a string in an unexpec print(f"The alphabet starts with {b!r}") # The alphabet starts with b'abc' print(f"The alphabet starts with {b.decode('utf-8')}") # The alphabet starts with abc +.. _code-str-unpack: + +Check that ``str`` is not unpacked [str-unpack] +--------------------------------------------------------- + +It can sometimes be surprising that ``str`` is iterable, especially when unpacking +in an assignment. + +Example: + +.. code-block:: python + + def print_dict(d: dict[str, str]) -> int: + # We meant to do d.items(), but instead we're unpacking the str keys of d + + # Error: Unpacking a string is disallowed + for k, v in d: + print(k, v) + .. _code-overload-overlap: Check that overloaded functions don't overlap [overload-overlap]