Repository: python/mypy

Issue:
Unsupported format character 'b'
Hello,

I have the following python code:

```python
a = b'%b' % b'123'
assert a == b'123'
```

Running mypy on it returns the following error:

```
teste.py:3: error: Unsupported format character 'b'
```

Which I guess is not correct since 'b' is a valid format for bytes:

https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting

This is my mypy.ini file:

```
[mypy]
python_version = 3.5
fast_parser = True
```

Currently using mypy 0.4.5


Discussion:
This is a mypy bug, and it's still present in 0.4.6.

Resolving patch:
diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py
--- a/mypy/checkstrformat.py
+++ b/mypy/checkstrformat.py
@@ -17,6 +17,8 @@
 from mypy import messages
 from mypy.messages import MessageBuilder
 
+FormatStringExpr = Union[StrExpr, BytesExpr, UnicodeExpr]
+
 
 class ConversionSpecifier:
     def __init__(self, key: str, flags: str, width: str, precision: str, type: str) -> None:
@@ -57,27 +59,31 @@ def __init__(self,
 
     # TODO: In Python 3, the bytes formatting has a more restricted set of options
     # compared to string formatting.
-    # TODO: Bytes formatting in Python 3 is only supported in 3.5 and up.
     def check_str_interpolation(self,
-                                str: Union[StrExpr, BytesExpr, UnicodeExpr],
+                                expr: FormatStringExpr,
                                 replacements: Expression) -> Type:
         """Check the types of the 'replacements' in a string interpolation
         expression: str % replacements
         """
-        specifiers = self.parse_conversion_specifiers(str.value)
-        has_mapping_keys = self.analyze_conversion_specifiers(specifiers, str)
+        specifiers = self.parse_conversion_specifiers(expr.value)
+        has_mapping_keys = self.analyze_conversion_specifiers(specifiers, expr)
+        if isinstance(expr, BytesExpr) and (3, 0) <= self.chk.options.python_version < (3, 5):
+            self.msg.fail('Bytes formatting is only supported in Python 3.5 and later',
+                          replacements)
+            return AnyType()
+
         if has_mapping_keys is None:
             pass  # Error was reported
         elif has_mapping_keys:
-            self.check_mapping_str_interpolation(specifiers, replacements)
+            self.check_mapping_str_interpolation(specifiers, replacements, expr)
         else:
-            self.check_simple_str_interpolation(specifiers, replacements)
+            self.check_simple_str_interpolation(specifiers, replacements, expr)
 
-        if isinstance(str, BytesExpr):
+        if isinstance(expr, BytesExpr):
             return self.named_type('builtins.bytes')
-        elif isinstance(str, UnicodeExpr):
+        elif isinstance(expr, UnicodeExpr):
             return self.named_type('builtins.unicode')
-        elif isinstance(str, StrExpr):
+        elif isinstance(expr, StrExpr):
             return self.named_type('builtins.str')
         else:
             assert False
@@ -115,8 +121,8 @@ def analyze_conversion_specifiers(self, specifiers: List[ConversionSpecifier],
         return has_key
 
     def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
-                                       replacements: Expression) -> None:
-        checkers = self.build_replacement_checkers(specifiers, replacements)
+                                       replacements: Expression, expr: FormatStringExpr) -> None:
+        checkers = self.build_replacement_checkers(specifiers, replacements, expr)
         if checkers is None:
             return
 
@@ -151,7 +157,8 @@ def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
                     check_type(rep_type)
 
     def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
-                                       replacements: Expression) -> None:
+                                        replacements: Expression,
+                                        expr: FormatStringExpr) -> None:
         if (isinstance(replacements, DictExpr) and
                 all(isinstance(k, (StrExpr, BytesExpr))
                     for k, v in replacements.items)):
@@ -168,7 +175,7 @@ def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
                     self.msg.key_not_in_mapping(specifier.key, replacements)
                     return
                 rep_type = mapping[specifier.key]
-                expected_type = self.conversion_type(specifier.type, replacements)
+                expected_type = self.conversion_type(specifier.type, replacements, expr)
                 if expected_type is None:
                     return
                 self.chk.check_subtype(rep_type, expected_type, replacements,
@@ -184,19 +191,20 @@ def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
                                    'expression has type', 'expected type for mapping is')
 
     def build_replacement_checkers(self, specifiers: List[ConversionSpecifier],
-                                   context: Context) -> List[Tuple[Callable[[Expression], None],
-                                                                   Callable[[Type], None]]]:
+                                   context: Context, expr: FormatStringExpr
+                                   ) -> List[Tuple[Callable[[Expression], None],
+                                                   Callable[[Type], None]]]:
         checkers = []  # type: List[Tuple[Callable[[Expression], None], Callable[[Type], None]]]
         for specifier in specifiers:
-            checker = self.replacement_checkers(specifier, context)
+            checker = self.replacement_checkers(specifier, context, expr)
             if checker is None:
                 return None
             checkers.extend(checker)
         return checkers
 
-    def replacement_checkers(self, specifier: ConversionSpecifier,
-                             context: Context) -> List[Tuple[Callable[[Expression], None],
-                                                             Callable[[Type], None]]]:
+    def replacement_checkers(self, specifier: ConversionSpecifier, context: Context,
+                             expr: FormatStringExpr) -> List[Tuple[Callable[[Expression], None],
+                                                                   Callable[[Type], None]]]:
         """Returns a list of tuples of two functions that check whether a replacement is
         of the right type for the specifier. The first functions take a node and checks
         its type in the right type context. The second function just checks a type.
@@ -208,12 +216,12 @@ def replacement_checkers(self, specifier: ConversionSpecifier,
         if specifier.precision == '*':
             checkers.append(self.checkers_for_star(context))
         if specifier.type == 'c':
-            c = self.checkers_for_c_type(specifier.type, context)
+            c = self.checkers_for_c_type(specifier.type, context, expr)
             if c is None:
                 return None
             checkers.append(c)
         elif specifier.type != '%':
-            c = self.checkers_for_regular_type(specifier.type, context)
+            c = self.checkers_for_regular_type(specifier.type, context, expr)
             if c is None:
                 return None
             checkers.append(c)
@@ -237,12 +245,13 @@ def check_expr(expr: Expression) -> None:
         return check_expr, check_type
 
     def checkers_for_regular_type(self, type: str,
-                                  context: Context) -> Tuple[Callable[[Expression], None],
-                                                             Callable[[Type], None]]:
+                                  context: Context,
+                                  expr: FormatStringExpr) -> Tuple[Callable[[Expression], None],
+                                                                   Callable[[Type], None]]:
         """Returns a tuple of check functions that check whether, respectively,
         a node or a type is compatible with 'type'. Return None in case of an
         """
-        expected_type = self.conversion_type(type, context)
+        expected_type = self.conversion_type(type, context, expr)
         if expected_type is None:
             return None
 
@@ -258,12 +267,13 @@ def check_expr(expr: Expression) -> None:
         return check_expr, check_type
 
     def checkers_for_c_type(self, type: str,
-                            context: Context) -> Tuple[Callable[[Expression], None],
-                                                       Callable[[Type], None]]:
+                            context: Context,
+                            expr: FormatStringExpr) -> Tuple[Callable[[Expression], None],
+                                                             Callable[[Type], None]]:
         """Returns a tuple of check functions that check whether, respectively,
         a node or a type is compatible with 'type' that is a character type
         """
-        expected_type = self.conversion_type(type, context)
+        expected_type = self.conversion_type(type, context, expr)
         if expected_type is None:
             return None
 
@@ -281,14 +291,28 @@ def check_expr(expr: Expression) -> None:
 
         return check_expr, check_type
 
-    def conversion_type(self, p: str, context: Context) -> Type:
+    def conversion_type(self, p: str, context: Context, expr: FormatStringExpr) -> Type:
         """Return the type that is accepted for a string interpolation
         conversion specifier type.
 
         Note that both Python's float (e.g. %f) and integer (e.g. %d)
         specifier types accept both float and integers.
         """
-        if p in ['s', 'r']:
+        if p == 'b':
+            if self.chk.options.python_version < (3, 5):
+                self.msg.fail("Format character 'b' is only supported in Python 3.5 and later",
+                              context)
+                return None
+            if not isinstance(expr, BytesExpr):
+                self.msg.fail("Format character 'b' is only supported on bytes patterns", context)
+                return None
+            return self.named_type('builtins.bytes')
+        elif p == 'a':
+            if self.chk.options.python_version < (3, 0):
+                self.msg.fail("Format character 'a' is only supported in Python 3", context)
+                return None
+            return AnyType()
+        elif p in ['s', 'r']:
             return AnyType()
         elif p in ['d', 'i', 'o', 'u', 'x', 'X',
                    'e', 'E', 'f', 'F', 'g', 'G']:


Test patch:
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -1047,6 +1047,22 @@ a = None # type: Any
 
 [case testStringInterpolationInvalidPlaceholder]
 '%W' % 1  # E: Unsupported format character 'W'
+'%b' % 1  # E: Format character 'b' is only supported on bytes patterns
+
+[case testStringInterPolationPython2]
+# flags: --python-version 2.7
+b'%b' % 1  # E: Format character 'b' is only supported in Python 3.5 and later
+b'%s' % 1
+b'%a' % 1  # E: Format character 'a' is only supported in Python 3
+
+[case testBytesInterpolationBefore35]
+# flags: --python-version 3.4
+b'%b' % 1  # E: Unsupported left operand type for % ("bytes")
+
+[case testBytesInterpolation]
+b'%b' % 1  # E: Incompatible types in string interpolation (expression has type "int", placeholder has type "bytes")
+b'%b' % b'1'
+b'%a' % 3
 
 [case testStringInterpolationWidth]
 '%2f' % 3.14