Repository: python/mypy

Issue:
bytes formatting incorrect in python 3
The following runs properly:

```
def foo(a: bytes, b:bytes) -> bytes:
    return b'%s:%s' % (a, b)
assert foo(b'a', b'b') == b'a:b'
```

However mypy complains: 

```
example.py: note: In function "foo":
example.py:2: error: Incompatible return value type (got "str", expected "bytes")
```



Discussion:
Oooh, good catch! I suspect the % checking code for bytes was too hastily adapted from the same for strings. There are many things wrong there: the checker always assumes the full formatting language used for str, while bytes have a separate (weaker) language. It always returns `builtins.str` as the return type (which is actually the error you're seeing). And I don't think the checker is invoked for unicode strings in PY2 mode...

(Also, though not affecting mypy's behavior, typeshed has no `__mod__` operator for bytes defined. It's new in Python 3.5, so mypy really should check python_version too.)


Resolving patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -965,8 +965,9 @@ def visit_op_expr(self, e: OpExpr) -> Type:
         if e.op == '*' and isinstance(e.left, ListExpr):
             # Expressions of form [...] * e get special type inference.
             return self.check_list_multiply(e)
-        if e.op == '%' and isinstance(e.left, (StrExpr, BytesExpr)):
-            return self.strfrm_checker.check_str_interpolation(cast(StrExpr, e.left), e.right)
+        if e.op == '%':
+            if isinstance(e.left, (StrExpr, BytesExpr, UnicodeExpr)):
+                return self.strfrm_checker.check_str_interpolation(e.left, e.right)
         left_type = self.accept(e.left)
 
         if e.op in nodes.op_methods:
diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py
--- a/mypy/checkstrformat.py
+++ b/mypy/checkstrformat.py
@@ -2,13 +2,13 @@
 
 import re
 
-from typing import cast, List, Tuple, Dict, Callable
+from typing import cast, List, Tuple, Dict, Callable, Union
 
 from mypy.types import (
     Type, AnyType, TupleType, Instance, UnionType
 )
 from mypy.nodes import (
-    Node, StrExpr, BytesExpr, TupleExpr, DictExpr, Context
+    Node, StrExpr, BytesExpr, UnicodeExpr, TupleExpr, DictExpr, Context
 )
 if False:
     # break import cycle only needed for mypy
@@ -55,7 +55,12 @@ def __init__(self,
         self.exprchk = exprchk
         self.msg = msg
 
-    def check_str_interpolation(self, str: StrExpr, replacements: Node) -> Type:
+    # 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],
+                                replacements: Node) -> Type:
         """Check the types of the 'replacements' in a string interpolation
         expression: str % replacements
         """
@@ -67,7 +72,15 @@ def check_str_interpolation(self, str: StrExpr, replacements: Node) -> Type:
             self.check_mapping_str_interpolation(specifiers, replacements)
         else:
             self.check_simple_str_interpolation(specifiers, replacements)
-        return self.named_type('builtins.str')
+
+        if isinstance(str, BytesExpr):
+            return self.named_type('builtins.bytes')
+        elif isinstance(str, UnicodeExpr):
+            return self.named_type('builtins.unicode')
+        elif isinstance(str, StrExpr):
+            return self.named_type('builtins.str')
+        else:
+            assert False
 
     def parse_conversion_specifiers(self, format: str) -> List[ConversionSpecifier]:
         key_regex = r'(\(([^()]*)\))?'  # (optional) parenthesised sequence of characters


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
@@ -1111,6 +1111,19 @@ main:4: error: Incompatible types in string interpolation (expression has type "
 [case testStringInterpolationSpaceKey]
 '%( )s' % {' ': 'foo'}
 
+[case testByteByteInterpolation]
+def foo(a: bytes, b: bytes):
+    b'%s:%s' % (a, b)
+foo(b'a', b'b') == b'a:b'
+
+[case testBytePercentInterpolationSupported]
+b'%s' % (b'xyz',)
+b'%(name)s' % {'name': 'jane'}
+b'%c' % (123)
+
+[case testUnicodeInterpolation_python2]
+u'%s' % (u'abc',)
+
 -- Lambdas
 -- -------