Repository: python/mypy
Pull request: Fixes error message for `f` string and `str-bytes-safe`
It does not mention `.format()` anymore.

Closes #10979

Problem statement:
Invalid `error: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior` when using formatted string literals
**Bug Report**

This particular check seems to be incompatible with formatted string literals.

**To Reproduce**

```
$ mypy -c 'x = b""; f"{x}"'
<string>:1: error: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
Found 1 error in 1 file (checked 1 source file)

$ mypy -c 'x = b""; f"{x:r}"'
<string>:1: error: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
Found 1 error in 1 file (checked 1 source file)

$ mypy --version
mypy 0.910
```

**Expected Behavior**

Should work in the same way as with `str.format()`:

```
$ mypy -c 'x = b""; "{x}".format(x=x)'
<string>:1: error: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
Found 1 error in 1 file (checked 1 source file)

$ mypy -c 'x = b""; "{x:r}".format(x=x)'
Success: no issues found in 1 source file
```

**Actual Behavior**

See **To Reproduce**

**Your Environment**

- Mypy version used: 0.910
- Mypy command-line flags: (see above)
- Mypy configuration options from `mypy.ini` (and other config files): none
- Python version used: 3.9.6
- Operating system and version: Fedora 34 x86_64

Hints from issue discussion:
Oh, it should be `f'{x!r}'`. Sorry, my fault.
For f-strings, the error message shouldn't talk about `.format()` though. Re-opening to track improving that error message.
Currently f-strings is represented as several str.format() calls on mypy AST.

Commit message: Fixes error message for `f` string and `str-bytes-safe`, do not mention `format()`

Patch to review:
diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py
index e1c4f71fa61b..2625cd9e7bcd 100644
--- a/mypy/checkstrformat.py
+++ b/mypy/checkstrformat.py
@@ -380,8 +380,9 @@ def perform_special_format_checks(self, spec: ConversionSpecifier, call: CallExp
                 if (has_type_component(actual_type, 'builtins.bytes') and
                         not custom_special_method(actual_type, '__str__')):
                     self.msg.fail(
-                        "On Python 3 '{}'.format(b'abc') produces \"b'abc'\", not 'abc'; "
-                        "use '{!r}'.format(b'abc') if this is desired behavior",
+                        'On Python 3 formatting string with "{}" and bytes argument '
+                        "produces result with \"b''\" prefix, not a regular string; "
+                        'use "{!r}" if this is desired behavior',
                         call, code=codes.STR_BYTES_PY3)
         if spec.flags:
             numeric_types = UnionType([self.named_type('builtins.int'),
@@ -836,8 +837,9 @@ def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Cont
             if self.chk.options.python_version >= (3, 0):
                 if has_type_component(typ, 'builtins.bytes'):
                     self.msg.fail(
-                        "On Python 3 '%s' % b'abc' produces \"b'abc'\", not 'abc'; "
-                        "use '%r' % b'abc' if this is desired behavior",
+                        'On Python 3 formatting string with "%s" and bytes argument '
+                        "produces result with \"b''\" prefix, not a regular string; "
+                        'use "%r" if this is desired behavior',
                         context, code=codes.STR_BYTES_PY3)
                     return False
             if self.chk.options.python_version < (3, 0):
diff --git a/mypyc/test-data/run-strings.test b/mypyc/test-data/run-strings.test
index 03a934b1bff8..19e7784cf549 100644
--- a/mypyc/test-data/run-strings.test
+++ b/mypyc/test-data/run-strings.test
@@ -218,8 +218,7 @@ def test_fstring_basics() -> None:
 
     x = bytes([1, 2, 3, 4])
     # assert f'bytes: {x}' == "bytes: b'\\x01\\x02\\x03\\x04'"
-    # error: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc';
-    #        use '{!r}'.format(b'abc') if this is desired behavior
+    # error: On Python 3 formatting string with "{}" and bytes argument produces result #        with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
 
     float_num = 123.4
     assert f'{float_num}' == '123.4'
@@ -354,7 +353,7 @@ def test_format_method_basics() -> None:
 def test_format_method_empty_braces() -> None:
     name = 'Eric'
     age = 14
-    
+
     assert 'Hello, {}!'.format(name) == 'Hello, Eric!'
     assert '{}'.format(name) == 'Eric'
     assert '{}! Hi!'.format(name) == 'Eric! Hi!'
diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test
index c7dbf81d909f..9cf8d6b22d16 100644
--- a/test-data/unit/check-errorcodes.test
+++ b/test-data/unit/check-errorcodes.test
@@ -641,8 +641,8 @@ def g() -> int:
 '%d' % 'no'  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")  [str-format]
 '%d + %d' % (1, 2, 3)  # E: Not all arguments converted during string formatting  [str-format]
 
-'{}'.format(b'abc')  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior  [str-bytes-safe]
-'%s' % b'abc'  # E: On Python 3 '%s' % b'abc' produces "b'abc'", not 'abc'; use '%r' % b'abc' if this is desired behavior  [str-bytes-safe]
+'{}'.format(b'abc')  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior  [str-bytes-safe]
+'%s' % b'abc'  # E: On Python 3 formatting string with "%s" and bytes argument produces result with "b''" prefix, not a regular string; use "%r" if this is desired behavior  [str-bytes-safe]
 [builtins fixtures/primitives.pyi]
 [typing fixtures/typing-medium.pyi]
 
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
index ff3a5efde6ad..07fc65a1cce5 100644
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -1183,8 +1183,8 @@ xb: bytes
 xs: str
 
 '%s' % xs   # OK
-'%s' % xb   # E: On Python 3 '%s' % b'abc' produces "b'abc'", not 'abc'; use '%r' % b'abc' if this is desired behavior
-'%(name)s' % {'name': b'value'}  # E: On Python 3 '%s' % b'abc' produces "b'abc'", not 'abc'; use '%r' % b'abc' if this is desired behavior
+'%s' % xb   # E: On Python 3 formatting string with "%s" and bytes argument produces result with "b''" prefix, not a regular string; use "%r" if this is desired behavior
+'%(name)s' % {'name': b'value'}  # E: On Python 3 formatting string with "%s" and bytes argument produces result with "b''" prefix, not a regular string; use "%r" if this is desired behavior
 [builtins fixtures/primitives.pyi]
 
 [case testStringInterpolationSBytesVsStrResultsPy2]
@@ -1617,22 +1617,32 @@ N = NewType('N', bytes)
 n: N
 
 '{}'.format(a)
-'{}'.format(b)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
-'{}'.format(x)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
-'{}'.format(n)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
+'{}'.format(b)  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
+'{}'.format(x)  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
+'{}'.format(n)  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
+
+f'{b}'  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
+f'{x}'  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
+f'{n}'  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
 
 class C(Generic[B]):
     x: B
     def meth(self) -> None:
-        '{}'.format(self.x)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
+        '{}'.format(self.x)  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
 
 def func(x: A) -> A:
-    '{}'.format(x)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'", not 'abc'; use '{!r}'.format(b'abc') if this is desired behavior
+    '{}'.format(x)  # E: On Python 3 formatting string with "{}" and bytes argument produces result with "b''" prefix, not a regular string; use "{!r}" if this is desired behavior
     return x
 
+'{!r}'.format(a)
 '{!r}'.format(b)
 '{!r}'.format(x)
 '{!r}'.format(n)
+f'{a}'
+f'{a!r}'
+f'{b!r}'
+f'{x!r}'
+f'{n!r}'
 
 class D(bytes):
     def __str__(self) -> str:


Code review comments:
diff_hunk: @@ -380,8 +380,9 @@ def perform_special_format_checks(self, spec: ConversionSpecifier, call: CallExp
                 if (has_type_component(actual_type, 'builtins.bytes') and
                         not custom_special_method(actual_type, '__str__')):
                     self.msg.fail(
-                        "On Python 3 '{}'.format(b'abc') produces \"b'abc'\", not 'abc'; "
-                        "use '{!r}'.format(b'abc') if this is desired behavior",
+                        'On Python 3 formatting string with "{}" and bytes argument '
path: mypy/checkstrformat.py
text: @user1:
The new wording is harder to understand. I'd suggest something closer to the original wording but using an f-string, like this:
```suggestion
                        "On Python 3 f'{b"abc"}' produces \"b'abc'\", not 'abc'; "
```
However, it looks like the same code path is used for f-strings and `.format()`. Is it easy to use separate messages for each of these?

@author:
> Is it easy to use separate messages for each of these?

Currently `mypy` uses the same exact `ast` to represent both `.format` and `f` strings. I can add new metadata to `CallExpr` (which is used for representation). But, I've decided that simply changing an error message is the way to go.

Maybe you can come up with better wording?

@user1:
Then I suggest we just always use the f-string version of the message if we're type checking Python 3, since I would assume that's the more likely case. Mypy doesn't support 3.5 any more. If this message is triggered when we're checking Python 2 code (can it?) we should still mention `.format()`.

@author:
> Then I suggest we just always use the f-string version of the message if we're type checking Python 3, since I would assume that's the more likely case.

I cannot agree, for example in my projects `f` string is not allowed by our styleguide: https://wemake-python-stylegui.de/en/latest/pages/usage/violations/consistency.html#wemake_python_styleguide.violations.consistency.FormattedStringViolation So, I know that a lot of people still use `.format()` 🙂 

But, we can use the common part like `{}` and `{!r}`. Maybe something like

```
On Python 3 '{b"abc"}' produces \"b'abc'\", not 'abc'; use "{b"abc"!r}" if this is desired behavior
```