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] 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/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..8d79545ae123 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2492,3 +2492,16 @@ 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) # 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 +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 398b007ce57d..a578735ec2c5 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -725,23 +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] - -[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 +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] -[builtins fixtures/dict.pyi] -[out] [case testUnionAlwaysTooMany] from typing import Union, Tuple