Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
3 changes: 3 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
2 changes: 1 addition & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
19 changes: 3 additions & 16 deletions test-data/unit/check-unions.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down