Skip to content

Commit c2a9642

Browse files
authored
Fix str unpack type propagation, use dedicated error code (#20325)
Resolves #13823. Supersedes #15511. See also #6406 To avail of this, try `--disable-error-code str-unpack`
1 parent 03b844d commit c2a9642

File tree

6 files changed

+41
-19
lines changed

6 files changed

+41
-19
lines changed

docs/source/error_code_list.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,25 @@ Warn about cases where a bytes object may be converted to a string in an unexpec
11511151
print(f"The alphabet starts with {b!r}") # The alphabet starts with b'abc'
11521152
print(f"The alphabet starts with {b.decode('utf-8')}") # The alphabet starts with abc
11531153
1154+
.. _code-str-unpack:
1155+
1156+
Check that ``str`` is not unpacked [str-unpack]
1157+
---------------------------------------------------------
1158+
1159+
It can sometimes be surprising that ``str`` is iterable, especially when unpacking
1160+
in an assignment.
1161+
1162+
Example:
1163+
1164+
.. code-block:: python
1165+
1166+
def print_dict(d: dict[str, str]) -> int:
1167+
# We meant to do d.items(), but instead we're unpacking the str keys of d
1168+
1169+
# Error: Unpacking a string is disallowed
1170+
for k, v in d:
1171+
print(k, v)
1172+
11541173
.. _code-overload-overlap:
11551174

11561175
Check that overloaded functions don't overlap [overload-overlap]

mypy/checker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4115,9 +4115,9 @@ def check_multi_assignment(
41154115
self.check_multi_assignment_from_union(
41164116
lvalues, rvalue, rvalue_type, context, infer_lvalue_type
41174117
)
4118-
elif isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str":
4119-
self.msg.unpacking_strings_disallowed(context)
41204118
else:
4119+
if isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str":
4120+
self.msg.unpacking_strings_disallowed(context)
41214121
self.check_multi_assignment_from_iterable(
41224122
lvalues, rvalue_type, context, infer_lvalue_type
41234123
)

mypy/errorcodes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ def __hash__(self) -> int:
215215
"General",
216216
default_enabled=False,
217217
)
218+
STR_UNPACK: Final[ErrorCode] = ErrorCode(
219+
"str-unpack", "Warn about expressions that unpack str", "General"
220+
)
218221
NAME_MATCH: Final = ErrorCode(
219222
"name-match", "Check that type definition has consistent naming", "General"
220223
)

mypy/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1144,7 +1144,7 @@ def wrong_number_values_to_unpack(
11441144
)
11451145

11461146
def unpacking_strings_disallowed(self, context: Context) -> None:
1147-
self.fail("Unpacking a string is disallowed", context)
1147+
self.fail("Unpacking a string is disallowed", context, code=codes.STR_UNPACK)
11481148

11491149
def type_not_iterable(self, type: Type, context: Context) -> None:
11501150
self.fail(f"{format_type(type, self.options)} object is not iterable", context)

test-data/unit/check-expressions.test

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,3 +2492,16 @@ x + T # E: Unsupported left operand type for + ("int")
24922492
T() # E: "TypeVar" not callable
24932493
[builtins fixtures/tuple.pyi]
24942494
[typing fixtures/typing-full.pyi]
2495+
2496+
[case testStringDisallowedUnpacking]
2497+
d: dict[str, str]
2498+
2499+
for a1, b1 in d: # E: Unpacking a string is disallowed
2500+
reveal_type(a1) # N: Revealed type is "builtins.str"
2501+
reveal_type(b1) # N: Revealed type is "builtins.str"
2502+
2503+
s = "foo"
2504+
a2, b2 = s # E: Unpacking a string is disallowed
2505+
reveal_type(a2) # N: Revealed type is "builtins.str"
2506+
reveal_type(b2) # N: Revealed type is "builtins.str"
2507+
[builtins fixtures/primitives.pyi]

test-data/unit/check-unions.test

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -725,23 +725,10 @@ bad: Union[int, str]
725725

726726
x, y = bad # E: "int" object is not iterable \
727727
# E: Unpacking a string is disallowed
728-
reveal_type(x) # N: Revealed type is "Any"
729-
reveal_type(y) # N: Revealed type is "Any"
730-
[out]
731-
732-
[case testStringDisallowedUnpacking]
733-
from typing import Dict
734-
735-
d: Dict[str, str]
736-
737-
for a, b in d: # E: Unpacking a string is disallowed
738-
pass
739-
740-
s = "foo"
741-
a, b = s # E: Unpacking a string is disallowed
728+
reveal_type(x) # N: Revealed type is "Union[Any, builtins.str]"
729+
reveal_type(y) # N: Revealed type is "Union[Any, builtins.str]"
730+
[builtins fixtures/primitives.pyi]
742731

743-
[builtins fixtures/dict.pyi]
744-
[out]
745732

746733
[case testUnionAlwaysTooMany]
747734
from typing import Union, Tuple

0 commit comments

Comments
 (0)