Repository: python/mypy
Pull request: Better support for string formatting
Fixes https://github.com/python/mypy/issues/1444
Fixes https://github.com/python/mypy/issues/2639
Fixes https://github.com/python/mypy/issues/7114

This PR does three related things:
* Fixes some corner cases in `%` formatting
* Tightens `bytes` vs `str` interactions (including those that are technically not errors)
* Adds type checking for `str.format()` calls

This also fixes few issues discovered by this PR during testing (notably a bug in f-string transformation), and adds/extends a bunch of docstring and comments to existing code. The implementation is mostly straightforward, there are few hacky things, but IMO nothing unreasonable.

Here are few comments:
* It was hard to keep the approach to `str.format()` calls purely regexp-based (as for `%` formatting), mostly because the former can be both nested and repeated (And we must support nested formatting because this is how we support f-strings). CPython itself uses a custom parser but it is huge, so I decided to have a mixed approach. This way we can keep code simple while still maintaining practically one-to-one compatibility with runtime behavior (the error messages are sometimes different however).
* This causes few hundreds of errors internally, I am not sure what to do with these. Most popular ones are:
  - Unicode upcast (`'%s' % u'...'` results in `unicode`, not `str`, on Python 2)
  - Using `'%s' % some_bytes` and/or `'{:s}'.format(some_bytes)` on Python 3
  - Unused arguments in `str.format()` call (probably because at runtime they are silently ignored)
* I added new error code for string interpolation/formatting and used it everywhere in old and new code. Potentially we might split out the `str` vs `bytes` errors into a separate error code, because technically they are not type errors, just suspicious/dangerous code.

Problem statement:
Type check str format calls
Mypy has some special logic to type check `%` string formatting. It would be nice to also get errors for invalid `format` calls. Examples:

```
'{} does not exist'.format(1, 2)   # too many arguments
'i like {target}'.format(tagret=name)   # misspelled keyword argument
```

This could be pretty useful since `format` calls often happen in exception and error handlers which are often not tested carefully.


Mypy complains about os.environ in string formatting
This kind of construct:

`"some silly string %(USER)s more silliness" % os.environ
`

provokes and error from mypy: 

`Format requires a mapping (expression has type "_Environ", expected type for mapping is Dict[Any, Any])
`

Should this usage really be flagged?


'%d' % SupportsInt fails
```python
i: SupportsInt = 5
s = '%d' % i
```
results in
```
Incompatible types in string interpolation (expression has type "SupportsInt", placeholder has type "Union[int, float]")
```

While in runtime it provides a reasonable `'5'`.

Hints from issue discussion:
This could be supported using the plugin system, but it we may have to extend it a little to support this use case.
Maybe a good test of the flexibility of the plugin system would be to see how it can be used to do some *existing* things that are currently done with custom checks in various places. This could include '%' formatting, and even TypedDict and NamedTuple.
> and even TypedDict and NamedTuple

This may be a too high bar. These two require lots of special-casing deep inside second and third semantic passes.
% formatting should be a good candidate but I agree with @ilevkivskyi that TypedDict and NamedTuple (maybe also Enum) might be too tricky, at least for now. I'd rather look at Django models or attrs (https://pypi.python.org/pypi/attrs/15.2.0) first.
This is a duplicate of #470 (although this one has more discussion).
Requiring a `dict` instance for `%` seems unnecessarily restrictive.

From @dmoisset on gitter:

> for % formatting, it probably should be Mapping[str, Any]

It looks like `Mapping[unicode, Any]` and `Mapping[str, Any]` should both be accepted in Python 2 mode.
I should probably have mentioned, I live in a mostly Python 2 world...

On Thu, Jan 5, 2017 at 2:26 PM, Jukka Lehtosalo <notifications@github.com>
wrote:

> Requiring a dict instance for % seems unnecessarily restrictive.
>
> From @dmoisset <https://github.com/dmoisset> on gitter:
>
> for % formatting, it probably should be Mapping[str, Any]
>
> It looks like Mapping[unicode, Any] and Mapping[str, Any] should both be
> accepted in Python 2 mode.
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/python/mypy/issues/2639#issuecomment-270748158>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/ADRNoTVGTA_PBcZVYFCRVFSM9DfuKUBCks5rPVIPgaJpZM4LcAyv>
> .
>

This should be a pretty straightforward fix to `checkstrformat` if you (or someone else) wants to submit a PR
@msullivan  Where do I have to add it? I can make a PR!
It looks like fixing this involves modifying the `conversion_type` function in `mypy/checkstrformat.py` and writing a test/tests for this.
@Patil2099 it looks like Ivan has fixed this as part of a broader overhaul of string formatting, though. Sorry!

Commit message: Remove fixed TODO item

Patch to review:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index b311037f9e19..81545b00959a 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -31,8 +31,7 @@
     DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr, AwaitExpr, YieldExpr,
     YieldFromExpr, TypedDictExpr, PromoteExpr, NewTypeExpr, NamedTupleExpr, TypeVarExpr,
     TypeAliasExpr, BackquoteExpr, EnumCallExpr, TypeAlias, SymbolNode, PlaceholderNode,
-    ARG_POS, ARG_OPT, ARG_NAMED, ARG_STAR, ARG_STAR2, LITERAL_TYPE, REVEAL_TYPE,
-    SYMBOL_FUNCBASE_TYPES
+    ARG_POS, ARG_OPT, ARG_NAMED, ARG_STAR, ARG_STAR2, LITERAL_TYPE, REVEAL_TYPE
 )
 from mypy.literals import literal
 from mypy import nodes
@@ -53,7 +52,7 @@
 from mypy import erasetype
 from mypy.checkmember import analyze_member_access, type_object_type
 from mypy.argmap import ArgTypeExpander, map_actuals_to_formals, map_formals_to_actuals
-from mypy.checkstrformat import StringFormatterChecker
+from mypy.checkstrformat import StringFormatterChecker, custom_special_method
 from mypy.expandtype import expand_type, expand_type_by_instance, freshen_function_type_vars
 from mypy.util import split_module_names
 from mypy.typevars import fill_typevars
@@ -333,6 +332,8 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
                 self.check_runtime_protocol_test(e)
             if e.callee.fullname == 'builtins.issubclass':
                 self.check_protocol_issubclass(e)
+        if isinstance(e.callee, MemberExpr) and e.callee.name == 'format':
+            self.check_str_format_call(e)
         ret_type = get_proper_type(ret_type)
         if isinstance(ret_type, UninhabitedType) and not ret_type.ambiguous:
             self.chk.binder.unreachable()
@@ -345,6 +346,19 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
             return AnyType(TypeOfAny.from_error)
         return ret_type
 
+    def check_str_format_call(self, e: CallExpr) -> None:
+        """More precise type checking for str.format() calls on literals."""
+        assert isinstance(e.callee, MemberExpr)
+        format_value = None
+        if isinstance(e.callee.expr, (StrExpr, UnicodeExpr)):
+            format_value = e.callee.expr.value
+        elif e.callee.expr in self.chk.type_map:
+            base_typ = try_getting_literal(self.chk.type_map[e.callee.expr])
+            if isinstance(base_typ, LiteralType) and isinstance(base_typ.value, str):
+                format_value = base_typ.value
+        if format_value is not None:
+            self.strfrm_checker.check_str_format_call(e, format_value)
+
     def method_fullname(self, object_type: Type, method_name: str) -> Optional[str]:
         """Convert a method name to a fully qualified name, based on the type of the object that
         it is invoked on. Return `None` if the name of `object_type` cannot be determined.
@@ -2020,8 +2034,8 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
                     # We suppress the error if there is a custom __eq__() method on either
                     # side. User defined (or even standard library) classes can define this
                     # to return True for comparisons between non-overlapping types.
-                    if (not custom_equality_method(left_type) and
-                            not custom_equality_method(right_type)):
+                    if (not custom_special_method(left_type, '__eq__') and
+                            not custom_special_method(right_type, '__eq__')):
                         # Also flag non-overlapping literals in situations like:
                         #    x: Literal['a', 'b']
                         #    if x == 'c':
@@ -4105,29 +4119,6 @@ def is_expr_literal_type(node: Expression) -> bool:
     return False
 
 
-def custom_equality_method(typ: Type) -> bool:
-    """Does this type have a custom __eq__() method?"""
-    typ = get_proper_type(typ)
-    if isinstance(typ, Instance):
-        method = typ.type.get('__eq__')
-        if method and isinstance(method.node, (SYMBOL_FUNCBASE_TYPES, Decorator, Var)):
-            if method.node.info:
-                return not method.node.info.fullname().startswith('builtins.')
-        return False
-    if isinstance(typ, UnionType):
-        return any(custom_equality_method(t) for t in typ.items)
-    if isinstance(typ, TupleType):
-        return custom_equality_method(tuple_fallback(typ))
-    if isinstance(typ, CallableType) and typ.is_type_obj():
-        # Look up __eq__ on the metaclass for class objects.
-        return custom_equality_method(typ.fallback)
-    if isinstance(typ, AnyType):
-        # Avoid false positives in uncertain cases.
-        return True
-    # TODO: support other types (see ExpressionChecker.has_member())?
-    return False
-
-
 def has_bytes_component(typ: Type) -> bool:
     """Is this the builtin bytes type, or a union that contains it?"""
     typ = get_proper_type(typ)
diff --git a/mypy/checkstrformat.py b/mypy/checkstrformat.py
index fb3553838682..02da2ed0784c 100644
--- a/mypy/checkstrformat.py
+++ b/mypy/checkstrformat.py
@@ -1,16 +1,32 @@
-"""Expression type checker. This file is conceptually part of ExpressionChecker and TypeChecker."""
+"""
+Format expression type checker.
+
+This file is conceptually part of ExpressionChecker and TypeChecker. Main functionality
+is located in StringFormatterChecker.check_str_format_call() for '{}'.format(), and in
+StringFormatterChecker.check_str_interpolation() for printf-style % interpolation.
+
+Note that although at runtime format strings are parsed using custom parsers,
+here we use a regerx-based approach. This way we 99% match runtime behaviour while keeping
+implementation simple.
+"""
 
 import re
 
-from typing import cast, List, Tuple, Dict, Callable, Union, Optional, Pattern
+from typing import (
+    cast, List, Tuple, Dict, Callable, Union, Optional, Pattern, Match, Set, Any
+)
 from typing_extensions import Final, TYPE_CHECKING
 
 from mypy.types import (
-    Type, AnyType, TupleType, Instance, UnionType, TypeOfAny, get_proper_type
+    Type, AnyType, TupleType, Instance, UnionType, TypeOfAny, get_proper_type, TypeVarType,
+    CallableType, LiteralType
 )
 from mypy.nodes import (
-    StrExpr, BytesExpr, UnicodeExpr, TupleExpr, DictExpr, Context, Expression, StarExpr
+    StrExpr, BytesExpr, UnicodeExpr, TupleExpr, DictExpr, Context, Expression, StarExpr, CallExpr,
+    IndexExpr, MemberExpr, TempNode, ARG_POS, ARG_STAR, ARG_NAMED, ARG_STAR2,
+    SYMBOL_FUNCBASE_TYPES, Decorator, Var, Node, MypyFile, ExpressionStmt, NameExpr, IntExpr
 )
+import mypy.errorcodes as codes
 
 if TYPE_CHECKING:
     # break import cycle only needed for mypy
@@ -18,12 +34,22 @@
     import mypy.checkexpr
 from mypy import message_registry
 from mypy.messages import MessageBuilder
+from mypy.maptype import map_instance_to_supertype
+from mypy.typeops import tuple_fallback
+from mypy.subtypes import is_subtype
+from mypy.parse import parse
 
 FormatStringExpr = Union[StrExpr, BytesExpr, UnicodeExpr]
 Checkers = Tuple[Callable[[Expression], None], Callable[[Type], None]]
+MatchMap = Dict[Tuple[int, int], Match[str]]  # span -> match
 
 
 def compile_format_re() -> Pattern[str]:
+    """Construct regexp to match format conversion specifiers in % interpolation.
+
+    See https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
+    The regexp is intentionally a bit wider to report better errors.
+    """
     key_re = r'(\(([^()]*)\))?'  # (optional) parenthesised sequence of characters.
     flags_re = r'([#0\-+ ]*)'  # (optional) sequence of flags.
     width_re = r'(\*|[1-9][0-9]*)?'  # (optional) minimum field width (* or numbers).
@@ -34,17 +60,94 @@ def compile_format_re() -> Pattern[str]:
     return re.compile(format_re)
 
 
+def compile_new_format_re(custom_spec: bool) -> Pattern[str]:
+    """Construct regexps to match format conversion specifiers in str.format() calls.
+
+    See After https://docs.python.org/3/library/string.html#formatspec for
+    specifications. The regexps are intentionally wider, to report better errors,
+    instead of just not matching.
+    """
+
+    # Field (optional) is an integer/identifier possibly followed by several .attr and [index].
+    field = r'(?P<field>(?P<key>[^.[!:]*)([^:!]+)?)'
+
+    # Conversion (optional) is ! followed by one of letters for forced repr(), str(), or ascii().
+    conversion = r'(?P<conversion>![^:])?'
+
+    # Format specification (optional) follows its own mini-language:
+    if not custom_spec:
+        # Fill and align is valid for all builtin types.
+        fill_align = r'(?P<fill_align>.?[<>=^])?'
+        # Number formatting options are only valid for int, float, complex, and Decimal,
+        # except if only width is given (it is valid for all types).
+        # This contains sign, flags (sign, # and/or 0), width, grouping (_ or ,) and precision.
+        num_spec = r'(?P<flags>[+\- ]?#?0?)(?P<width>\d+)?[_,]?(?P<precision>\.\d+)?'
+        # The last element is type.
+        type = r'(?P<type>.)?'  # only some are supported, but we want to give a better error
+        format_spec = r'(?P<format_spec>:' + fill_align + num_spec + type + r')?'
+    else:
+        # Custom types can define their own form_spec using __format__().
+        format_spec = r'(?P<format_spec>:.*)?'
+
+    return re.compile(field + conversion + format_spec)
+
+
 FORMAT_RE = compile_format_re()  # type: Final
+FORMAT_RE_NEW = compile_new_format_re(False)  # type: Final
+FORMAT_RE_NEW_CUSTOM = compile_new_format_re(True)  # type: Final
+DUMMY_FIELD_NAME = '__dummy_name__'  # type: Final
+
+# Format types supported by str.format() for builtin classes.
+SUPPORTED_TYPES_NEW = ['b', 'c', 'd', 'e', 'E', 'f', 'F',
+                       'g', 'G', 'n', 'o', 's', 'x', 'X', '%']  # type: Final
+
+# Types that require int, float, or SupportsInt.
+NUMERIC_TYPES_OLD = ['d', 'i', 'o', 'u', 'x', 'X',
+                     'e', 'E', 'f', 'F', 'g', 'G']  # type: Final
+NUMERIC_TYPES_NEW = ['b', 'd', 'o', 'e', 'E', 'f', 'F',
+                     'g', 'G', 'x', 'X', '%']  # type: Final
 
 
 class ConversionSpecifier:
     def __init__(self, key: Optional[str],
-                 flags: str, width: str, precision: str, type: str) -> None:
+                 flags: str, width: str, precision: str, type: str,
+                 format_spec: Optional[str] = None,
+                 conversion: Optional[str] = None,
+                 field: Optional[str] = None) -> None:
         self.key = key
         self.flags = flags
         self.width = width
         self.precision = precision
         self.type = type
+        # Used only for str.format() calls (it may be custom for types with __format__()).
+        self.format_spec = format_spec
+        self.non_standard_format_spec = False
+        # Used only for str.format() calls.
+        self.conversion = conversion
+        # Full formatted expression (i.e. key plus following attributes and/or indexes).
+        # Used only for str.format() calls.
+        self.field = field
+
+    @classmethod
+    def from_match(cls, match_obj: Match[str],
+                   non_standard_spec: bool = False) -> 'ConversionSpecifier':
+        """Construct specifier from match object resulted from parsing str.format() call."""
+        match = cast(Any, match_obj)  # TODO: remove this once typeshed is fixed.
+        if non_standard_spec:
+            spec = cls(match.group('key'),
+                       flags='', width='', precision='', type='',
+                       format_spec=match.group('format_spec'),
+                       conversion=match.group('conversion'),
+                       field=match.group('field'))
+            spec.non_standard_format_spec = True
+            return spec
+        # Replace unmatched optional groups with empty matches (for convenience).
+        return cls(match.group('key'),
+                   flags=match.group('flags') or '', width=match.group('width') or '',
+                   precision=match.group('precision') or '', type=match.group('type') or '',
+                   format_spec=match.group('format_spec'),
+                   conversion=match.group('conversion'),
+                   field=match.group('field'))
 
     def has_key(self) -> bool:
         return self.key is not None
@@ -74,6 +177,388 @@ def __init__(self,
         self.chk = chk
         self.exprchk = exprchk
         self.msg = msg
+        # This flag is used to track Python 2 corner case where for example
+        # '%s, %d' % (u'abc', 42) returns u'abc, 42' (i.e. unicode, not a string).
+        self.unicode_upcast = False
+
+    def check_str_format_call(self, call: CallExpr, format_value: str) -> None:
+        """Perform more precise checks for str.format() calls when possible.
+
+        Currently the checks are performed for:
+          * Actual string literals
+          * Literal types with string values
+          * Final names with string values
+
+        The checks that we currently perform:
+          * Check generic validity (e.g. unmatched { or }, and {} in invalid positions)
+          * Check consistency of specifiers' auto-numbering
+          * Verify that replacements can be found for all conversion specifiers,
+            and all arguments were used
+          * Non-standard format specs are only allowed for types with custom __format__
+          * Type check replacements with accessors applied (if any).
+          * Verify that specifier type is known and matches replacement type
+          * Perform special checks for some specifier types:
+            - 'c' requires a single character string
+            - 's' must not accept bytes
+            - non-empty flags are only allowed for numeric types
+        """
+        conv_specs = self.parse_format_value(format_value, call)
+        if conv_specs is None:
+            return
+        if not self.auto_generate_keys(conv_specs, call):
+            return
+        self.check_specs_in_format_call(call, conv_specs, format_value)
+
+    def parse_format_value(self, format_value: str, ctx: Context,
+                           nested: bool = False) -> Optional[List[ConversionSpecifier]]:
+        """Parse format string into list of conversion specifiers.
+
+        The specifiers may be nested (two levels maximum), in this case they are ordered as
+        '{0:{1}}, {2:{3}{4}}'. Return None in case of an error.
+        """
+        top_targets = self.find_non_escaped_targets(format_value, ctx)
+        if top_targets is None:
+            return None
+
+        result = []  # type: List[ConversionSpecifier]
+        for target in top_targets:
+            match = FORMAT_RE_NEW.fullmatch(target)
+            if match:
+                conv_spec = ConversionSpecifier.from_match(match)
+            else:
+                custom_match = FORMAT_RE_NEW_CUSTOM.fullmatch(target)
+                if custom_match:
+                    conv_spec = ConversionSpecifier.from_match(custom_match,
+                                                               non_standard_spec=True)
+                else:
+                    self.msg.fail('Invalid conversion specifier in format string',
+                                  ctx, code=codes.STRING_FORMATTING)
+                    return None
+
+            if conv_spec.key and ('{' in conv_spec.key or '}' in conv_spec.key):
+                self.msg.fail('Conversion value must not contain { or }',
+                              ctx, code=codes.STRING_FORMATTING)
+                return None
+            result.append(conv_spec)
+
+            # Parse nested conversions that are allowed in format specifier.
+            if (conv_spec.format_spec and conv_spec.non_standard_format_spec and
+                    ('{' in conv_spec.format_spec or '}' in conv_spec.format_spec)):
+                if nested:
+                    self.msg.fail('Formatting nesting must be at most two levels deep',
+                                  ctx, code=codes.STRING_FORMATTING)
+                    return None
+                sub_conv_specs = self.parse_format_value(conv_spec.format_spec[1:], ctx=ctx,
+                                                         nested=True)
+                if sub_conv_specs is None:
+                    return None
+                result.extend(sub_conv_specs)
+        return result
+
+    def find_non_escaped_targets(self, format_value: str, ctx: Context) -> Optional[List[str]]:
+        """Return list of raw (un-parsed) format specifiers in format string.
+
+        Format specifiers don't include enclosing braces. We don't use regexp for
+        this because they don't work well with nested/repeated patterns
+        (both greedy and non-greedy), and these are heavily used internally for
+        representation of f-strings.
+
+        Return None in case of an error.
+        """
+        result = []
+        next_spec = ''
+        pos = 0
+        nesting = 0
+        while pos < len(format_value):
+            c = format_value[pos]
+            if not nesting:
+                # Skip any paired '{{' and '}}', enter nesting on '{', report error on '}'.
+                if c == '{':
+                    if pos < len(format_value) - 1 and format_value[pos + 1] == '{':
+                        pos += 1
+                    else:
+                        nesting = 1
+                if c == '}':
+                    if pos < len(format_value) - 1 and format_value[pos + 1] == '}':
+                        pos += 1
+                    else:
+                        self.msg.fail('Invalid conversion specifier in format string:'
+                                      ' unexpected }', ctx, code=codes.STRING_FORMATTING)
+                        return None
+            else:
+                # Adjust nesting level, then either continue adding chars or move on.
+                if c == '{':
+                    nesting += 1
+                if c == '}':
+                    nesting -= 1
+                if nesting:
+                    next_spec += c
+                else:
+                    result.append(next_spec)
+                    next_spec = ''
+            pos += 1
+        if nesting:
+            self.msg.fail('Invalid conversion specifier in format string:'
+                          ' unmatched {', ctx, code=codes.STRING_FORMATTING)
+            return None
+        return result
+
+    def check_specs_in_format_call(self, call: CallExpr,
+                                   specs: List[ConversionSpecifier], format_value: str) -> None:
+        """Perform pairwise checks for conversion specifiers vs their replacements.
+
+        The core logic for format checking is implemented in this method.
+        """
+        assert all(s.key for s in specs), "Keys must be auto-generated first!"
+        replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs])
+        assert len(replacements) == len(specs)
+        for spec, repl in zip(specs, replacements):
+            repl = self.apply_field_accessors(spec, repl, ctx=call)
+            actual_type = repl.type if isinstance(repl, TempNode) else self.chk.type_map.get(repl)
+            assert actual_type is not None
+
+            # Special case custom formatting.
+            if (spec.format_spec and spec.non_standard_format_spec and
+                    # Exclude "dynamic" specifiers (i.e. containing nested formatting).
+                    not ('{' in spec.format_spec or '}' in spec.format_spec)):
+                if not custom_special_method(actual_type, '__format__') or spec.conversion:
+                    # TODO: add support for some custom specs like datetime?
+                    self.msg.fail('Unrecognized format'
+                                  ' specification "{}"'.format(spec.format_spec[1:]),
+                                  call, code=codes.STRING_FORMATTING)
+                    continue
+            # Adjust expected and actual types.
+            if not spec.type:
+                expected_type = AnyType(TypeOfAny.special_form)  # type: Optional[Type]
+            else:
+                assert isinstance(call.callee, MemberExpr)
+                if isinstance(call.callee.expr, (StrExpr, UnicodeExpr)):
+                    format_str = call.callee.expr
+                else:
+                    format_str = StrExpr(format_value)
+                expected_type = self.conversion_type(spec.type, call, format_str,
+                                                     format_call=True)
+            if spec.conversion is not None:
+                # If the explicit conversion is given, then explicit conversion is called _first_.
+                if spec.conversion[1] not in 'rsa':
+                    self.msg.fail('Invalid conversion type "{}",'
+                                  ' must be one of "r", "s" or "a"'.format(spec.conversion[1]),
+                                  call, code=codes.STRING_FORMATTING)
+                actual_type = self.named_type('builtins.str')
+
+            # Perform the checks for given types.
+            if expected_type is None:
+                continue
+            self.check_placeholder_type(actual_type, expected_type, call)
+            self.perform_special_format_checks(spec, call, repl, actual_type, expected_type)
+
+    def perform_special_format_checks(self, spec: ConversionSpecifier, call: CallExpr,
+                                      repl: Expression, actual_type: Type,
+                                      expected_type: Type) -> None:
+        # TODO: try refactoring to combine this logic with % formatting.
+        if spec.type == 'c':
+            if isinstance(repl, (StrExpr, BytesExpr)) and len(cast(StrExpr, repl).value) != 1:
+                self.msg.requires_int_or_char(call, format_call=True)
+            c_typ = get_proper_type(self.chk.type_map[repl])
+            if isinstance(c_typ, Instance) and c_typ.last_known_value:
+                c_typ = c_typ.last_known_value
+            if isinstance(c_typ, LiteralType) and isinstance(c_typ.value, str):
+                if len(c_typ.value) != 1:
+                    self.msg.requires_int_or_char(call, format_call=True)
+        if (not spec.type or spec.type == 's') and not spec.conversion:
+            if self.chk.options.python_version >= (3, 0):
+                if has_type_component(actual_type, 'builtins.bytes'):
+                    self.msg.fail("On Python 3 '{}'.format(b'abc') produces \"b'abc'\";"
+                                  " use !r if this is a desired behavior", call,
+                                  code=codes.STRING_FORMATTING)
+        if spec.flags:
+            numeric_types = UnionType([self.named_type('builtins.int'),
+                                       self.named_type('builtins.float')])
+            if (spec.type and spec.type not in NUMERIC_TYPES_NEW or
+                    not spec.type and not is_subtype(actual_type, numeric_types)):
+                self.msg.fail('Numeric flags are only allowed for numeric types', call,
+                              code=codes.STRING_FORMATTING)
+
+    def find_replacements_in_call(self, call: CallExpr,
+                                  keys: List[str]) -> List[Expression]:
+        """Find replacement expression for every specifier in str.format() call.
+
+        In case of an error use TempNode(AnyType).
+        """
+        result = []  # type: List[Expression]
+        used = set()  # type: Set[Expression]
+        for key in keys:
+            if key.isdecimal():
+                expr = self.get_expr_by_position(int(key), call)
+                if not expr:
+                    self.msg.fail('Cannot find replacement for positional'
+                                  ' format specifier {}'.format(key), call,
+                                  code=codes.STRING_FORMATTING)
+                    expr = TempNode(AnyType(TypeOfAny.from_error))
+            else:
+                expr = self.get_expr_by_name(key, call)
+                if not expr:
+                    self.msg.fail('Cannot find replacement for named'
+                                  ' format specifier "{}"'.format(key), call,
+                                  code=codes.STRING_FORMATTING)
+                    expr = TempNode(AnyType(TypeOfAny.from_error))
+            result.append(expr)
+            if not isinstance(expr, TempNode):
+                used.add(expr)
+        # Strictly speaking not using all replacements is not a type error, but most likely
+        # a typo in user code, so we show an error like we do for % formatting.
+        total_explicit = len([kind for kind in call.arg_kinds if kind in (ARG_POS, ARG_NAMED)])
+        if len(used) < total_explicit:
+            self.msg.too_many_string_formatting_arguments(call)
+        return result
+
+    def get_expr_by_position(self, pos: int, call: CallExpr) -> Optional[Expression]:
+        """Get positional replacement expression from '{0}, {1}'.format(x, y, ...) call.
+
+        If the type is from *args, return TempNode(<item type>). Return None in case of
+        an error.
+        """
+        pos_args = [arg for arg, kind in zip(call.args, call.arg_kinds) if kind == ARG_POS]
+        if pos < len(pos_args):
+            return pos_args[pos]
+        star_args = [arg for arg, kind in zip(call.args, call.arg_kinds) if kind == ARG_STAR]
+        if not star_args:
+            return None
+
+        # Fall back to *args when present in call.
+        star_arg = star_args[0]
+        varargs_type = get_proper_type(self.chk.type_map[star_arg])
+        if (not isinstance(varargs_type, Instance) or not
+                varargs_type.type.has_base('typing.Iterable')):
+            # Error should be already reported.
+            return TempNode(AnyType(TypeOfAny.special_form))
+        iter_info = self.chk.named_generic_type('typing.Iterable',
+                                                [AnyType(TypeOfAny.special_form)]).type
+        return TempNode(map_instance_to_supertype(varargs_type, iter_info).args[0])
+
+    def get_expr_by_name(self, key: str, call: CallExpr) -> Optional[Expression]:
+        """Get named replacement expression from '{name}'.format(name=...) call.
+
+        If the type is from **kwargs, return TempNode(<item type>). Return None in case of
+        an error.
+        """
+        named_args = [arg for arg, kind, name in zip(call.args, call.arg_kinds, call.arg_names)
+                      if kind == ARG_NAMED and name == key]
+        if named_args:
+            return named_args[0]
+        star_args_2 = [arg for arg, kind in zip(call.args, call.arg_kinds) if kind == ARG_STAR2]
+        if not star_args_2:
+            return None
+        star_arg_2 = star_args_2[0]
+        kwargs_type = get_proper_type(self.chk.type_map[star_arg_2])
+        if (not isinstance(kwargs_type, Instance) or not
+                kwargs_type.type.has_base('typing.Mapping')):
+            # Error should be already reported.
+            return TempNode(AnyType(TypeOfAny.special_form))
+        any_type = AnyType(TypeOfAny.special_form)
+        mapping_info = self.chk.named_generic_type('typing.Mapping',
+                                                   [any_type, any_type]).type
+        return TempNode(map_instance_to_supertype(kwargs_type, mapping_info).args[1])
+
+    def auto_generate_keys(self, all_specs: List[ConversionSpecifier],
+                           ctx: Context) -> bool:
+        """Translate '{} {name} {}' to '{0} {name} {1}'.
+
+        Return True if generation was successful, otherwise report an error and return false.
+        """
+        some_defined = any(s.key and s.key.isdecimal() for s in all_specs)
+        all_defined = all(bool(s.key) for s in all_specs)
+        if some_defined and not all_defined:
+            self.msg.fail('Cannot combine automatic field numbering and'
+                          ' manual field specification', ctx, code=codes.STRING_FORMATTING)
+            return False
+        if all_defined:
+            return True
+        next_index = 0
+        for spec in all_specs:
+            if not spec.key:
+                str_index = str(next_index)
+                spec.key = str_index
+                # Update also the full field (i.e. turn {.x} into {0.x}).
+                if not spec.field:
+                    spec.field = str_index
+                else:
+                    spec.field = str_index + spec.field
+                next_index += 1
+        return True
+
+    def apply_field_accessors(self, spec: ConversionSpecifier, repl: Expression,
+                              ctx: Context) -> Expression:
+        """Transform and validate expr in '{.attr[item]}'.format(expr) into expr.attr['item'].
+
+        If validation fails, return TempNode(AnyType).
+        """
+        assert spec.key, "Keys must be auto-generated first!"
+        if spec.field == spec.key:
+            return repl
+        assert spec.field
+
+        # This is a bit of a dirty trick, but it looks like this is the simplest way.
+        temp_errors = self.msg.clean_copy().errors
+        dummy = DUMMY_FIELD_NAME + spec.field[len(spec.key):]
+        temp_ast = parse(dummy, fnam='<format>', module=None,
+                         options=self.chk.options, errors=temp_errors)  # type: Node
+        if temp_errors.is_errors():
+            self.msg.fail('Syntax error in format specifier "{}"'.format(spec.field),
+                          ctx, code=codes.STRING_FORMATTING)
+            return TempNode(AnyType(TypeOfAny.from_error))
+
+        # These asserts are guaranteed by the original regexp.
+        assert isinstance(temp_ast, MypyFile)
+        temp_ast = temp_ast.defs[0]
+        assert isinstance(temp_ast, ExpressionStmt)
+        temp_ast = temp_ast.expr
+        if not self.validate_and_transform_accessors(temp_ast, repl, spec, ctx=ctx):
+            return TempNode(AnyType(TypeOfAny.from_error))
+
+        # Check if there are any other errors (like missing members).
+        # TODO: fix column to point to actual start of the format specifier _within_ string.
+        temp_ast.line = ctx.line
+        temp_ast.column = ctx.column
+        self.exprchk.accept(temp_ast)
+        return temp_ast
+
+    def validate_and_transform_accessors(self, temp_ast: Expression, original_repl: Expression,
+                                         spec: ConversionSpecifier, ctx: Context) -> bool:
+        """Validate and transform (in-place) format field accessors.
+
+        On error, report it and return False.
+        """
+        if not isinstance(temp_ast, (MemberExpr, IndexExpr)):
+            self.msg.fail('Only index and member expressions are allowed in'
+                          ' format field accessors; got "{}"'.format(spec.field),
+                          ctx, code=codes.STRING_FORMATTING)
+            return False
+        if isinstance(temp_ast, MemberExpr):
+            node = temp_ast.expr
+        else:
+            node = temp_ast.base
+            if not isinstance(temp_ast.index, (NameExpr, IntExpr)):
+                assert spec.key, "Call this method only after auto-generating keys!"
+                assert spec.field
+                self.msg.fail('Invalid index expression in format field'
+                              ' accessor "{}"'.format(spec.field[len(spec.key):]), ctx,
+                              code=codes.STRING_FORMATTING)
+                return False
+            if isinstance(temp_ast.index, NameExpr):
+                temp_ast.index = StrExpr(temp_ast.index.name)
+        if isinstance(node, NameExpr) and node.name == DUMMY_FIELD_NAME:
+            # Replace it with the actual replacement expression.
+            assert isinstance(temp_ast, (IndexExpr, MemberExpr))  # XXX: this is redundant
+            if isinstance(temp_ast, IndexExpr):
+                temp_ast.base = original_repl
+            else:
+                temp_ast.expr = original_repl
+            return True
+        node.line = ctx.line
+        node.column = ctx.column
+        return self.validate_and_transform_accessors(node, original_repl=original_repl,
+                                                     spec=spec, ctx=ctx)
 
     # TODO: In Python 3, the bytes formatting has a more restricted set of options
     # compared to string formatting.
@@ -81,16 +566,17 @@ def check_str_interpolation(self,
                                 expr: FormatStringExpr,
                                 replacements: Expression) -> Type:
         """Check the types of the 'replacements' in a string interpolation
-        expression: str % replacements
+        expression: str % replacements.
         """
         self.exprchk.accept(expr)
         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)
+                          replacements, code=codes.STRING_FORMATTING)
             return AnyType(TypeOfAny.from_error)
 
+        self.unicode_upcast = False
         if has_mapping_keys is None:
             pass  # Error was reported
         elif has_mapping_keys:
@@ -103,6 +589,8 @@ def check_str_interpolation(self,
         elif isinstance(expr, UnicodeExpr):
             return self.named_type('builtins.unicode')
         elif isinstance(expr, StrExpr):
+            if self.unicode_upcast:
+                return self.named_type('builtins.unicode')
             return self.named_type('builtins.str')
         else:
             assert False
@@ -133,6 +621,7 @@ def analyze_conversion_specifiers(self, specifiers: List[ConversionSpecifier],
 
     def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
                                        replacements: Expression, expr: FormatStringExpr) -> None:
+        """Check % string interpolation with positional specifiers '%s, %d' % ('yes, 42')."""
         checkers = self.build_replacement_checkers(specifiers, replacements, expr)
         if checkers is None:
             return
@@ -173,11 +662,17 @@ def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
     def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
                                         replacements: Expression,
                                         expr: FormatStringExpr) -> None:
+        """Check % string interpolation with names specifiers '%(name)s' % {'name': 'John'}."""
         if (isinstance(replacements, DictExpr) and
                 all(isinstance(k, (StrExpr, BytesExpr, UnicodeExpr))
                     for k, v in replacements.items)):
             mapping = {}  # type: Dict[str, Type]
             for k, v in replacements.items:
+                if self.chk.options.python_version >= (3, 0) and isinstance(expr, BytesExpr):
+                    # Special case: for bytes formatting keys must be bytes.
+                    if not isinstance(k, BytesExpr):
+                        self.msg.fail('Dictionary keys in bytes formatting must be bytes,'
+                                      ' not strings', expr, code=codes.STRING_FORMATTING)
                 key_str = cast(FormatStringExpr, k).value
                 mapping[key_str] = self.accept(v)
 
@@ -196,15 +691,40 @@ def check_mapping_str_interpolation(self, specifiers: List[ConversionSpecifier],
                 self.chk.check_subtype(rep_type, expected_type, replacements,
                                        message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
                                        'expression has type',
-                                       'placeholder with key \'%s\' has type' % specifier.key)
+                                       'placeholder with key \'%s\' has type' % specifier.key,
+                                       code=codes.STRING_FORMATTING)
+                if specifier.type == 's':
+                    self.check_s_special_cases(expr, rep_type, expr)
         else:
             rep_type = self.accept(replacements)
-            any_type = AnyType(TypeOfAny.special_form)
-            dict_type = self.chk.named_generic_type('builtins.dict',
-                                                    [any_type, any_type])
+            dict_type = self.build_dict_type(expr)
             self.chk.check_subtype(rep_type, dict_type, replacements,
                                    message_registry.FORMAT_REQUIRES_MAPPING,
-                                   'expression has type', 'expected type for mapping is')
+                                   'expression has type', 'expected type for mapping is',
+                                   code=codes.STRING_FORMATTING)
+
+    def build_dict_type(self, expr: FormatStringExpr) -> Type:
+        """Build expected mapping type for right operand in % formatting."""
+        any_type = AnyType(TypeOfAny.special_form)
+        if self.chk.options.python_version >= (3, 0):
+            if isinstance(expr, BytesExpr):
+                bytes_type = self.chk.named_generic_type('builtins.bytes', [])
+                return self.chk.named_generic_type('typing.Mapping',
+                                                   [bytes_type, any_type])
+            elif isinstance(expr, StrExpr):
+                str_type = self.chk.named_generic_type('builtins.str', [])
+                return self.chk.named_generic_type('typing.Mapping',
+                                                   [str_type, any_type])
+            else:
+                assert False, "There should not be UnicodeExpr on Python 3"
+        else:
+            str_type = self.chk.named_generic_type('builtins.str', [])
+            unicode_type = self.chk.named_generic_type('builtins.unicode', [])
+            str_map = self.chk.named_generic_type('typing.Mapping',
+                                                  [str_type, any_type])
+            unicode_map = self.chk.named_generic_type('typing.Mapping',
+                                                      [unicode_type, any_type])
+            return UnionType.make_union([str_map, unicode_map])
 
     def build_replacement_checkers(self, specifiers: List[ConversionSpecifier],
                                    context: Context, expr: FormatStringExpr
@@ -243,13 +763,14 @@ def replacement_checkers(self, specifier: ConversionSpecifier, context: Context,
 
     def checkers_for_star(self, context: Context) -> Checkers:
         """Returns a tuple of check functions that check whether, respectively,
-        a node or a type is compatible with a star in a conversion specifier
+        a node or a type is compatible with a star in a conversion specifier.
         """
         expected = self.named_type('builtins.int')
 
         def check_type(type: Type) -> None:
             expected = self.named_type('builtins.int')
-            self.chk.check_subtype(type, expected, context, '* wants int')
+            self.chk.check_subtype(type, expected, context, '* wants int',
+                                   code=codes.STRING_FORMATTING)
 
         def check_expr(expr: Expression) -> None:
             type = self.accept(expr, expected)
@@ -257,21 +778,27 @@ def check_expr(expr: Expression) -> None:
 
         return check_expr, check_type
 
+    def check_placeholder_type(self, typ: Type, expected_type: Type, context: Context) -> None:
+        self.chk.check_subtype(typ, expected_type, context,
+                               message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
+                               'expression has type', 'placeholder has type',
+                               code=codes.STRING_FORMATTING)
+
     def checkers_for_regular_type(self, type: str,
                                   context: Context,
                                   expr: FormatStringExpr) -> Optional[Checkers]:
         """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
+        a node or a type is compatible with 'type'. Return None in case of an error.
         """
         expected_type = self.conversion_type(type, context, expr)
         if expected_type is None:
             return None
 
-        def check_type(type: Type) -> None:
+        def check_type(typ: Type) -> None:
             assert expected_type is not None
-            self.chk.check_subtype(type, expected_type, context,
-                              message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
-                              'expression has type', 'placeholder has type')
+            self.check_placeholder_type(typ, expected_type, context)
+            if type == 's':
+                self.check_s_special_cases(expr, typ, context)
 
         def check_expr(expr: Expression) -> None:
             type = self.accept(expr, expected_type)
@@ -279,11 +806,30 @@ def check_expr(expr: Expression) -> None:
 
         return check_expr, check_type
 
+    def check_s_special_cases(self, expr: FormatStringExpr, typ: Type, context: Context) -> None:
+        """Additional special cases for %s in bytes vs string context."""
+        if isinstance(expr, StrExpr):
+            # Couple special cases for string formatting.
+            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'\";"
+                                  " use %r if this is a desired behavior", context,
+                                  code=codes.STRING_FORMATTING)
+            if self.chk.options.python_version < (3, 0):
+                if has_type_component(typ, 'builtins.unicode'):
+                    self.unicode_upcast = True
+        if isinstance(expr, BytesExpr):
+            # A special case for bytes formatting: b'%s' actually requires bytes on Python 3.
+            if self.chk.options.python_version >= (3, 0):
+                if has_type_component(typ, 'builtins.str'):
+                    self.msg.fail("On Python 3 b'%s' requires bytes, not string", context,
+                                  code=codes.STRING_FORMATTING)
+
     def checkers_for_c_type(self, type: str,
                             context: Context,
                             expr: FormatStringExpr) -> Optional[Checkers]:
         """Returns a tuple of check functions that check whether, respectively,
-        a node or a type is compatible with 'type' that is a character type
+        a node or a type is compatible with 'type' that is a character type.
         """
         expected_type = self.conversion_type(type, context, expr)
         if expected_type is None:
@@ -291,9 +837,7 @@ def checkers_for_c_type(self, type: str,
 
         def check_type(type: Type) -> None:
             assert expected_type is not None
-            self.chk.check_subtype(type, expected_type, context,
-                              message_registry.INCOMPATIBLE_TYPES_IN_STR_INTERPOLATION,
-                              'expression has type', 'placeholder has type')
+            self.check_placeholder_type(type, expected_type, context)
 
         def check_expr(expr: Expression) -> None:
             """int, or str with length 1"""
@@ -304,34 +848,40 @@ def check_expr(expr: Expression) -> None:
 
         return check_expr, check_type
 
-    def conversion_type(self, p: str, context: Context, expr: FormatStringExpr) -> Optional[Type]:
-        """Return the type that is accepted for a string interpolation
-        conversion specifier type.
+    def conversion_type(self, p: str, context: Context, expr: FormatStringExpr,
+                        format_call: bool = False) -> Optional[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.
+
+        The 'format_call' argument indicates whether this type came from % interpolation or from
+        a str.format() call, the meaning of few formatting types are different.
         """
-        if p == 'b':
+        NUMERIC_TYPES = NUMERIC_TYPES_NEW if format_call else NUMERIC_TYPES_OLD
+        if p == 'b' and not format_call:
             if self.chk.options.python_version < (3, 5):
                 self.msg.fail("Format character 'b' is only supported in Python 3.5 and later",
-                              context)
+                              context, code=codes.STRING_FORMATTING)
                 return None
             if not isinstance(expr, BytesExpr):
-                self.msg.fail("Format character 'b' is only supported on bytes patterns", context)
+                self.msg.fail("Format character 'b' is only supported on bytes patterns", context,
+                              code=codes.STRING_FORMATTING)
                 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)
+                self.msg.fail("Format character 'a' is only supported in Python 3", context,
+                              code=codes.STRING_FORMATTING)
                 return None
-            # todo: return type object?
+            # TODO: return type object?
             return AnyType(TypeOfAny.special_form)
         elif p in ['s', 'r']:
             return AnyType(TypeOfAny.special_form)
-        elif p in ['d', 'i', 'o', 'u', 'x', 'X',
-                   'e', 'E', 'f', 'F', 'g', 'G']:
+        elif p in NUMERIC_TYPES:
             return UnionType([self.named_type('builtins.int'),
-                              self.named_type('builtins.float')])
+                              self.named_type('builtins.float'),
+                              self.named_type('typing.SupportsInt')])
         elif p in ['c']:
             return UnionType([self.named_type('builtins.int'),
                               self.named_type('builtins.float'),
@@ -353,3 +903,45 @@ def named_type(self, name: str) -> Instance:
     def accept(self, expr: Expression, context: Optional[Type] = None) -> Type:
         """Type check a node. Alias for TypeChecker.accept."""
         return self.chk.expr_checker.accept(expr, context)
+
+
+def has_type_component(typ: Type, fullname: str) -> bool:
+    """Is this a specific instance type, or a union that contains it?
+
+    We use this ad-hoc function instead of a proper visitor or subtype check
+    because some str vs bytes errors are strictly speaking not runtime errors,
+    but rather highly counter-intuitive behavior. This is similar to what is used for
+    --strict-equality.
+    """
+    typ = get_proper_type(typ)
+    if isinstance(typ, Instance):
+        return typ.type.has_base(fullname)
+    elif isinstance(typ, TypeVarType):
+        return (has_type_component(typ.upper_bound, fullname) or
+                any(has_type_component(v, fullname) for v in typ.values))
+    elif isinstance(typ, UnionType):
+        return any(has_type_component(t, fullname) for t in typ.relevant_items())
+    return False
+
+
+def custom_special_method(typ: Type, name: str) -> bool:
+    """Does this type have a custom special method such as __format__() or __eq__()?"""
+    typ = get_proper_type(typ)
+    if isinstance(typ, Instance):
+        method = typ.type.get(name)
+        if method and isinstance(method.node, (SYMBOL_FUNCBASE_TYPES, Decorator, Var)):
+            if method.node.info:
+                return not method.node.info.fullname().startswith('builtins.')
+        return False
+    if isinstance(typ, UnionType):
+        return any(custom_special_method(t, name) for t in typ.items)
+    if isinstance(typ, TupleType):
+        return custom_special_method(tuple_fallback(typ), name)
+    if isinstance(typ, CallableType) and typ.is_type_obj():
+        # Look up __method__ on the metaclass for class objects.
+        return custom_special_method(typ.fallback, name)
+    if isinstance(typ, AnyType):
+        # Avoid false positives in uncertain cases.
+        return True
+    # TODO: support other types (see ExpressionChecker.has_member())?
+    return False
diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py
index 01c2d4fdfd1c..f39409272cf7 100644
--- a/mypy/errorcodes.py
+++ b/mypy/errorcodes.py
@@ -77,6 +77,9 @@ def __str__(self) -> str:
     'General')  # type: Final
 VALID_NEWTYPE = ErrorCode(
     'valid-newtype', "Check that argument 2 to NewType is valid", 'General')  # type: Final
+STRING_FORMATTING = ErrorCode(
+    'str-format', "Check that string formatting/interpolation is type-safe",
+    'General')  # type: Final
 
 # These error codes aren't enable by default.
 NO_UNTYPED_DEF = ErrorCode(
diff --git a/mypy/fastparse.py b/mypy/fastparse.py
index a5518228df94..800f6141e02f 100644
--- a/mypy/fastparse.py
+++ b/mypy/fastparse.py
@@ -1146,8 +1146,8 @@ def visit_FormattedValue(self, n: ast3.FormattedValue) -> Expression:
         format_method.set_line(format_string)
         result_expression = CallExpr(format_method,
                                      [val_exp, format_spec_exp],
-                                     [ARG_POS],
-                                     [None])
+                                     [ARG_POS, ARG_POS],
+                                     [None, None])
         return self.set_line(result_expression, n)
 
     # Bytes(bytes s)
diff --git a/mypy/messages.py b/mypy/messages.py
index 6827610761b8..362b84b1bd9d 100644
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -738,25 +738,33 @@ def first_argument_for_super_must_be_type(self, actual: Type, context: Context)
         self.fail('Argument 1 for "super" must be a type object; got {}'.format(type_str), context)
 
     def too_few_string_formatting_arguments(self, context: Context) -> None:
-        self.fail('Not enough arguments for format string', context)
+        self.fail('Not enough arguments for format string', context,
+                  code=codes.STRING_FORMATTING)
 
     def too_many_string_formatting_arguments(self, context: Context) -> None:
-        self.fail('Not all arguments converted during string formatting', context)
+        self.fail('Not all arguments converted during string formatting', context,
+                  code=codes.STRING_FORMATTING)
 
     def unsupported_placeholder(self, placeholder: str, context: Context) -> None:
-        self.fail('Unsupported format character \'%s\'' % placeholder, context)
+        self.fail('Unsupported format character \'%s\'' % placeholder, context,
+                  code=codes.STRING_FORMATTING)
 
     def string_interpolation_with_star_and_key(self, context: Context) -> None:
-        self.fail('String interpolation contains both stars and mapping keys', context)
+        self.fail('String interpolation contains both stars and mapping keys', context,
+                  code=codes.STRING_FORMATTING)
 
-    def requires_int_or_char(self, context: Context) -> None:
-        self.fail('%c requires int or char', context)
+    def requires_int_or_char(self, context: Context,
+                             format_call: bool = False) -> None:
+        self.fail('"{}c" requires int or char'.format(':' if format_call else '%'),
+                  context, code=codes.STRING_FORMATTING)
 
     def key_not_in_mapping(self, key: str, context: Context) -> None:
-        self.fail('Key \'%s\' not found in mapping' % key, context)
+        self.fail('Key \'%s\' not found in mapping' % key, context,
+                  code=codes.STRING_FORMATTING)
 
     def string_interpolation_mixing_key_and_non_keys(self, context: Context) -> None:
-        self.fail('String interpolation mixes specifier with and without mapping keys', context)
+        self.fail('String interpolation mixes specifier with and without mapping keys', context,
+                  code=codes.STRING_FORMATTING)
 
     def cannot_determine_type(self, name: str, context: Context) -> None:
         self.fail("Cannot determine type of '%s'" % name, context, code=codes.HAS_TYPE)
diff --git a/mypyc/emit.py b/mypyc/emit.py
index b58c72fa05d9..ddc450d86337 100644
--- a/mypyc/emit.py
+++ b/mypyc/emit.py
@@ -622,7 +622,7 @@ def emit_box(self, src: str, dest: str, typ: RType, declare_dest: bool = False,
                     inner_name = self.temp_name()
                     self.emit_box('{}.f{}'.format(src, i), inner_name, typ.types[i],
                                   declare_dest=True)
-                    self.emit_line('PyTuple_SET_ITEM({}, {}, {});'.format(dest, i, inner_name, i))
+                    self.emit_line('PyTuple_SET_ITEM({}, {}, {});'.format(dest, i, inner_name))
         else:
             assert not typ.is_unboxed
             # Type is boxed -- trivially just assign.
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
index 234837091bf9..6e28259aff65 100644
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -1130,10 +1130,10 @@ i, f, s, t = None, None, None, None # type: (int, float, str, Tuple[int])
 '%f' % f
 '%s' % s
 '%d' % (f,)
-'%d' % (s,) # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float]")
+'%d' % (s,) # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
 '%d' % t
-'%d' % s  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float]")
-'%f' % s  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float]")
+'%d' % s  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'%f' % s  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
 [builtins fixtures/primitives.pyi]
 
 [case testStringInterpolationSAcceptsAnyType]
@@ -1142,13 +1142,34 @@ i, o, s = None, None, None # type: (int, object, str)
 '%s %s %s' % (i, o, s)
 [builtins fixtures/primitives.pyi]
 
+[case testStringInterpolationSBytesVsStrErrorPy3]
+xb: bytes
+xs: str
+
+'%s' % xs   # OK
+'%s' % xb   # E: On Python 3 '%s' % b'abc' produces "b'abc'"; use %r if this is a desired behavior
+'%(name)s' % {'name': b'value'}  # E: On Python 3 '%s' % b'abc' produces "b'abc'"; use %r if this is a desired behavior
+[builtins fixtures/primitives.pyi]
+
+[case testStringInterpolationSBytesVsStrResultsPy2]
+# flags: --python-version 2.7
+xs = 'x'
+xu = u'x'
+
+reveal_type('%s' % xu)  # N: Revealed type is 'builtins.unicode'
+reveal_type('%s, %d' % (u'abc', 42))  # N: Revealed type is 'builtins.unicode'
+reveal_type('%(key)s' % {'key': xu})  # N: Revealed type is 'builtins.unicode'
+reveal_type('%r' % xu)  # N: Revealed type is 'builtins.str'
+reveal_type('%s' % xs)  # N: Revealed type is 'builtins.str'
+[builtins fixtures/primitives.pyi]
+
 [case testStringInterpolationCount]
 '%d %d' % 1  # E: Not enough arguments for format string
 '%d %d' % (1, 2)
 '%d %d' % (1, 2, 3)  # E: Not all arguments converted during string formatting
 t = 1, 's'
 '%d %s' % t
-'%s %d' % t  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float]")
+'%s %d' % t  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
 '%d' % t  # E: Not all arguments converted during string formatting
 [builtins fixtures/primitives.pyi]
 
@@ -1216,13 +1237,13 @@ b'%a' % 3
 [case testStringInterpolationC]
 '%c' % 1
 '%c' % 's'
-'%c' % ''  # E: %c requires int or char
-'%c' % 'ab'  # E: %c requires int or char
+'%c' % ''  # E: "%c" requires int or char
+'%c' % 'ab'  # E: "%c" requires int or char
 [builtins fixtures/primitives.pyi]
 
 [case testStringInterpolationMappingTypes]
 '%(a)d %(b)s' % {'a': 1, 'b': 's'}
-'%(a)d %(b)s' % {'a': 's', 'b': 1}  # E: Incompatible types in string interpolation (expression has type "str", placeholder with key 'a' has type "Union[int, float]")
+'%(a)d %(b)s' % {'a': 's', 'b': 1}  # E: Incompatible types in string interpolation (expression has type "str", placeholder with key 'a' has type "Union[int, float, SupportsInt]")
 b'%(x)s' % {b'x': b'data'}
 [builtins fixtures/primitives.pyi]
 
@@ -1232,23 +1253,25 @@ b'%(x)s' % {b'x': b'data'}
 '%(q)d' % {'a': 1, 'b': 2, 'c': 3}  # E: Key 'q' not found in mapping
 '%(a)d %%' % {'a': 1}
 
-[builtins fixtures/dict.pyi]
+[builtins fixtures/primitives.pyi]
 
 [case testStringInterpolationMappingDictTypes]
 from typing import Any, Dict
 a = None # type: Any
 ds, do, di = None, None, None # type: Dict[str, int], Dict[object, int], Dict[int, int]
-'%(a)' % 1  # E: Format requires a mapping (expression has type "int", expected type for mapping is "Dict[Any, Any]")
+'%(a)' % 1  # E: Format requires a mapping (expression has type "int", expected type for mapping is "Mapping[str, Any]")
 '%()d' % a
 '%()d' % ds
-'%()d' % do
-[builtins fixtures/dict.pyi]
+'%()d' % do  # E: Format requires a mapping (expression has type "Dict[object, int]", expected type for mapping is "Mapping[str, Any]")
+b'%()d' % ds  # E: Format requires a mapping (expression has type "Dict[str, int]", expected type for mapping is "Mapping[bytes, Any]")
+[builtins fixtures/primitives.pyi]
 
-[case testStringInterpolationMappingInvalidDictTypes-skip]
+[case testStringInterpolationMappingInvalidDictTypesPy2]
+# flags: --py2 --no-strict-optional
 from typing import Any, Dict
 di = None # type: Dict[int, int]
-'%()d' % di  # E: Format requires a mapping (expression has type Dict[int, int], expected type for mapping is Dict[str, Any])
-[builtins fixtures/dict.pyi]
+'%()d' % di  # E: Format requires a mapping (expression has type "Dict[int, int]", expected type for mapping is "Union[Mapping[str, Any], Mapping[unicode, Any]]")
+[builtins_py2 fixtures/python2.pyi]
 
 [case testStringInterpolationMappingInvalidSpecifiers]
 '%(a)d %d' % 1  # E: String interpolation mixes specifier with and without mapping keys
@@ -1259,7 +1282,7 @@ di = None # type: Dict[int, int]
 '%(a)1d' % {'a': 1}
 '%(a).1d' % {'a': 1}
 '%(a)#1.1ld' % {'a': 1}
-[builtins fixtures/dict.pyi]
+[builtins fixtures/primitives.pyi]
 
 [case testStringInterpolationFloatPrecision]
 '%.f' % 1.2
@@ -1268,8 +1291,8 @@ di = None # type: Dict[int, int]
 '%.3f' % 'x'
 [builtins fixtures/primitives.pyi]
 [out]
-main:3: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float]")
-main:4: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float]")
+main:3: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+main:4: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
 
 [case testStringInterpolationSpaceKey]
 '%( )s' % {' ': 'foo'}
@@ -1285,7 +1308,8 @@ x = (1, 2)
 
 [case testBytePercentInterpolationSupported]
 b'%s' % (b'xyz',)
-b'%(name)s' % {'name': 'jane'}
+b'%(name)s' % {'name': b'jane'}  # E: Dictionary keys in bytes formatting must be bytes, not strings
+b'%(name)s' % {b'name': 'jane'}  # E: On Python 3 b'%s' requires bytes, not string
 b'%c' % (123)
 
 [case testUnicodeInterpolation_python2]
@@ -1299,6 +1323,306 @@ def f(t: Tuple[int, ...]) -> None:
 [builtins fixtures/primitives.pyi]
 
 
+-- str.format() calls
+-- ------------------
+
+[case testFormatCallParseErrors]
+'}'.format()  # E: Invalid conversion specifier in format string: unexpected }
+'{'.format()  # E: Invalid conversion specifier in format string: unmatched {
+
+'}}'.format()  # OK
+'{{'.format()  # OK
+
+'{{}}}'.format()  # E: Invalid conversion specifier in format string: unexpected }
+'{{{}}'.format()  # E: Invalid conversion specifier in format string: unexpected }
+
+'{}}{{}'.format()  # E: Invalid conversion specifier in format string: unexpected }
+'{{{}:{}}}'.format(0)  # E: Cannot find replacement for positional format specifier 1
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallValidationErrors]
+'{!}}'.format(0)  # E: Invalid conversion specifier in format string: unexpected }
+'{!x}'.format(0)  # E: Invalid conversion type "x", must be one of "r", "s" or "a"
+'{!:}'.format(0)  # E: Invalid conversion specifier in format string
+
+'{{}:s}'.format(0)  # E: Invalid conversion specifier in format string: unexpected }
+'{{}.attr}'.format(0)  # E: Invalid conversion specifier in format string: unexpected }
+'{{}[key]}'.format(0)  # E: Invalid conversion specifier in format string: unexpected }
+
+'{ {}:s}'.format()  # E: Conversion value must not contain { or }
+'{ {}.attr}'.format()  # E: Conversion value must not contain { or }
+'{ {}[key]}'.format()  # E: Conversion value must not contain { or }
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallEscaping]
+'{}'.format()  # E: Cannot find replacement for positional format specifier 0
+'{}'.format(0)  # OK
+
+'{{}}'.format()  # OK
+'{{}}'.format(0)  # E: Not all arguments converted during string formatting
+
+'{{{}}}'.format()  # E: Cannot find replacement for positional format specifier 0
+'{{{}}}'.format(0)  # OK
+
+'{{}} {} {{}}'.format(0)  # OK
+'{{}} {:d} {{}} {:d}'.format('a', 'b')  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+
+'foo({}, {}) == {{}} ({{}} expected)'.format(0)  # E: Cannot find replacement for positional format specifier 1
+'foo({}, {}) == {{}} ({{}} expected)'.format(0, 1)  # OK
+'foo({}, {}) == {{}} ({{}} expected)'.format(0, 1, 2)  # E: Not all arguments converted during string formatting
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallNestedFormats]
+'{:{}{}}'.format(42, '*')  # E: Cannot find replacement for positional format specifier 2
+'{:{}{}}'.format(42, '*', '^')  # OK
+'{:{}{}}'.format(42, '*', '^', 0)  # E: Not all arguments converted during string formatting
+
+# NOTE: we don't check format specifiers that contain { or } at all
+'{:{{}}}'.format()  # E: Cannot find replacement for positional format specifier 0
+
+'{:{:{}}}'.format()  # E: Formatting nesting must be at most two levels deep
+'{:{{}:{}}}'.format()  # E: Invalid conversion specifier in format string: unexpected }
+
+'{!s:{fill:d}{align}}'.format(42, fill='*', align='^')  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallAutoNumbering]
+'{}, {{}}, {0}'.format()  # E: Cannot combine automatic field numbering and manual field specification
+'{0}, {1}, {}'.format()  # E: Cannot combine automatic field numbering and manual field specification
+
+'{0}, {1}, {0}'.format(1, 2, 3)  # E: Not all arguments converted during string formatting
+'{}, {other:+d}, {}'.format(1, 2, other='no')  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'{0}, {other}, {}'.format()  # E: Cannot combine automatic field numbering and manual field specification
+
+'{:{}}, {:{:.5d}{}}'.format(1, 2, 3, 'a', 5)  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallMatchingPositional]
+'{}'.format(positional='no')  # E: Cannot find replacement for positional format specifier 0 \
+                              # E: Not all arguments converted during string formatting
+'{.x}, {}, {}'.format(1, 'two', 'three')  # E: "int" has no attribute "x"
+'Reverse {2.x}, {1}, {0}'.format(1, 2, 'three')  # E: "str" has no attribute "x"
+''.format(1, 2)  # E: Not all arguments converted during string formatting
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallMatchingNamed]
+'{named}'.format(0)  # E: Cannot find replacement for named format specifier "named" \
+                     # E: Not all arguments converted during string formatting
+'{one.x}, {two}'.format(one=1, two='two')  # E: "int" has no attribute "x"
+'{one}, {two}, {.x}'.format(1, one='two', two='three')  # E: "int" has no attribute "x"
+''.format(stuff='yes')  # E: Not all arguments converted during string formatting
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallMatchingVarArg]
+from typing import List
+args: List[int] = []
+'{}, {}'.format(1, 2, *args)  # Don't flag this because args may be empty
+
+strings: List[str]
+'{:d}, {[0].x}'.format(*strings)  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]") \
+                                  # E: "str" has no attribute "x"
+# TODO: this is a runtime error, but error message is confusing
+'{[0][:]:d}'.format(*strings)  # E: Syntax error in format specifier "0[0]["
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallMatchingKwArg]
+from typing import Dict
+kwargs: Dict[str, str] = {}
+'{one}, {two}'.format(one=1, two=2, **kwargs)  # Don't flag this because args may be empty
+
+'{stuff:.3d}'.format(**kwargs)  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'{stuff[0]:f}, {other}'.format(**kwargs)  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'{stuff[0]:c}'.format(**kwargs)
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallCustomFormatSpec]
+class Bad:
+    ...
+class Good:
+    def __format__(self, spec: str) -> str: ...
+
+'{:OMG}'.format(Good())
+'{:OMG}'.format(Bad())  # E: Unrecognized format specification "OMG"
+'{!s:OMG}'.format(Good())  # E: Unrecognized format specification "OMG"
+'{:{}OMG{}}'.format(Bad(), 'too', 'dynamic')
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallFormatTypes]
+'{:x}'.format(42)
+'{:E}'.format(42)
+'{:g}'.format(42)
+'{:x}'.format('no')  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'{:E}'.format('no')  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'{:g}'.format('no')  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+
+'{:s}'.format(42)
+'{:s}'.format('yes')
+
+'{:z}'.format('what')  # E: Unsupported format character 'z'
+'{:Z}'.format('what')  # E: Unsupported format character 'Z'
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallFormatTypesChar]
+'{:c}'.format(42)
+'{:c}'.format('no')  # E: ":c" requires int or char
+'{:c}'.format('c')
+
+class C:
+    ...
+'{:c}'.format(C())  # E: Incompatible types in string interpolation (expression has type "C", placeholder has type "Union[int, float, str]")
+x: str
+'{:c}'.format(x)
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallFormatTypesBytes]
+from typing import Union, TypeVar, NewType, Generic
+
+A = TypeVar('A', str, bytes)
+B = TypeVar('B', bound=bytes)
+
+x: Union[str, bytes]
+a: str
+b: bytes
+
+N = NewType('N', bytes)
+n: N
+
+'{}'.format(a)
+'{}'.format(b)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'"; use !r if this is a desired behavior
+'{}'.format(x)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'"; use !r if this is a desired behavior
+'{}'.format(n)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'"; use !r if this is a 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'"; use !r if this is a desired behavior
+
+def func(x: A) -> A:
+    '{}'.format(x)  # E: On Python 3 '{}'.format(b'abc') produces "b'abc'"; use !r if this is a desired behavior
+    return x
+
+'{!r}'.format(b)
+'{!r}'.format(x)
+'{!r}'.format(n)
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallFormatTypesBytesNotPy2]
+# flags: --py2
+from typing import Union, TypeVar, NewType, Generic
+
+A = TypeVar('A', str, unicode)
+B = TypeVar('B', bound=str)
+
+x = ''  # type: Union[str, unicode]
+a = ''
+b = b''
+
+N = NewType('N', str)
+n = N(b'')
+
+'{}'.format(a)
+'{}'.format(b)
+'{}'.format(x)
+'{}'.format(n)
+
+u'{}'.format(a)
+u'{}'.format(b)
+u'{}'.format(x)
+u'{}'.format(n)
+
+class C(Generic[B]):
+    x = None  # type: B
+    def meth(self):
+        # type: () -> None
+        '{}'.format(self.x)
+
+def func(x):
+    # type: (A) -> A
+    '{}'.format(x)
+    return x
+
+'{!r}'.format(b)
+'{!r}'.format(x)
+'{!r}'.format(n)
+[builtins_py2 fixtures/python2.pyi]
+
+[case testFormatCallFinal]
+from typing_extensions import Final
+
+FMT: Final = '{.x}, {:{:d}}'
+
+FMT.format(1, 2, 'no')  # E: "int" has no attribute "x" \
+                        # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallFinalChar]
+from typing_extensions import Final
+
+GOOD: Final = 'c'
+BAD: Final = 'no'
+OK: Final[str] = '...'
+
+'{:c}'.format(GOOD)
+'{:c}'.format(BAD)  # E: ":c" requires int or char
+'{:c}'.format(OK)
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallForcedConversions]
+'{!r}'.format(42)
+'{!s}'.format(42)
+'{!s:d}'.format(42)  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+'{!s:s}'.format('OK')
+'{} and {!x}'.format(0, 1)  # E: Invalid conversion type "x", must be one of "r", "s" or "a"
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallAccessorsBasic]
+from typing import Any
+x: Any
+
+'{.x:{[0]}}'.format('yes', 42)  # E: "str" has no attribute "x" \
+                                # E: Value of type "int" is not indexable
+
+'{.1+}'.format(x)  # E: Syntax error in format specifier "0.1+"
+'{name.x[x]()[x]:.2f}'.format(name=x)  # E: Only index and member expressions are allowed in format field accessors; got "name.x[x]()[x]"
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallAccessorsIndices]
+from typing_extensions import TypedDict
+
+class User(TypedDict):
+    id: int
+    name: str
+
+u: User
+'{user[name]:.3f}'.format(user=u)  # E: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]")
+
+def f() -> str: ...
+'{[f()]}'.format(u)  # E: Invalid index expression in format field accessor "[f()]"
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallFlags]
+'{:#}'.format(42)
+
+'{:#}'.format('no')  # E: Numeric flags are only allowed for numeric types
+'{!s:#}'.format(42)  # E: Numeric flags are only allowed for numeric types
+
+'{:#s}'.format(42)  # E: Numeric flags are only allowed for numeric types
+'{:+s}'.format(42)  # E: Numeric flags are only allowed for numeric types
+
+'{:+d}'.format(42)
+'{:#d}'.format(42)
+[builtins fixtures/primitives.pyi]
+
+[case testFormatCallSpecialCases]
+'{:08b}'.format(int('3'))
+
+class Decimal:
+    def __int__(self) -> int: ...
+
+'{:+d}'.format(Decimal())  # This is OK
+'{:%}'.format(0.001)
+[builtins fixtures/primitives.pyi]
+
 -- Lambdas
 -- -------
 
diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi
index 07f1daf14193..c1636c197410 100644
--- a/test-data/unit/fixtures/primitives.pyi
+++ b/test-data/unit/fixtures/primitives.pyi
@@ -1,6 +1,7 @@
 # builtins stub with non-generic primitive types
-from typing import Generic, TypeVar, Sequence, Iterator
+from typing import Generic, TypeVar, Sequence, Iterator, Mapping
 T = TypeVar('T')
+V = TypeVar('V')
 
 class object:
     def __init__(self) -> None: pass
@@ -24,12 +25,18 @@ class str(Sequence[str]):
     def __iter__(self) -> Iterator[str]: pass
     def __contains__(self, other: object) -> bool: pass
     def __getitem__(self, item: int) -> str: pass
-    def format(self, *args) -> str: pass
+    def format(self, *args, **kwargs) -> str: pass
 class bytes(Sequence[int]):
     def __iter__(self) -> Iterator[int]: pass
     def __contains__(self, other: object) -> bool: pass
     def __getitem__(self, item: int) -> int: pass
 class bytearray: pass
 class tuple(Generic[T]): pass
+class list(Sequence[T]):
+    def __iter__(self) -> Iterator[T]: pass
+    def __contains__(self, other: object) -> bool: pass
+    def __getitem__(self, item: int) -> T: pass
+class dict(Mapping[T, V]):
+    def __iter__(self) -> Iterator[T]: pass
 class function: pass
 class ellipsis: pass
diff --git a/test-data/unit/fixtures/python2.pyi b/test-data/unit/fixtures/python2.pyi
index eb7c4df14e34..734a8ac8ee57 100644
--- a/test-data/unit/fixtures/python2.pyi
+++ b/test-data/unit/fixtures/python2.pyi
@@ -11,11 +11,16 @@ class type:
 class function: pass
 
 class int: pass
-class str: pass
-class unicode: pass
+class str:
+    def format(self, *args, **kwars) -> str: ...
+class unicode:
+    def format(self, *args, **kwars) -> unicode: ...
 class bool: pass
 
 T = TypeVar('T')
+S = TypeVar('S')
 class list(Iterable[T], Generic[T]): pass
+class tuple(Iterable[T]): pass
+class dict(Generic[T, S]): pass
 
 # Definition of None is implicit
diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi
index 520d73b2e34c..bd80aff83cc9 100644
--- a/test-data/unit/lib-stub/typing.pyi
+++ b/test-data/unit/lib-stub/typing.pyi
@@ -38,6 +38,9 @@ class Sequence(Iterable[T_co]):
 class Mapping(Generic[T, T_co]):
     def __getitem__(self, key: T) -> T_co: pass
 
+class SupportsInt(Protocol):
+    def __int__(self) -> int: pass
+
 # This is an unofficial extension.
 def final(meth: T) -> T: pass
 
diff --git a/test-data/unit/parse.test b/test-data/unit/parse.test
index 09a908c5d20f..65a8145c64b6 100644
--- a/test-data/unit/parse.test
+++ b/test-data/unit/parse.test
@@ -3345,7 +3345,8 @@ MypyFile:1(
               StrExpr({:{}})    
               format)           
             Args(               
-              NameExpr(x))))))))
+              NameExpr(x)
+              StrExpr())))))))
 
 [case testFStringWithConversion]
 x = 'mypy'
@@ -3368,7 +3369,8 @@ MypyFile:1(
               StrExpr({!r:{}})  
               format)           
             Args(               
-              NameExpr(x))))))))
+              NameExpr(x)
+              StrExpr())))))))
 
 [case testFStringWithOnlyFormatSpecifier]
 x = 'mypy'
@@ -3391,7 +3393,8 @@ MypyFile:1(
               StrExpr({:{}})    
               format)           
             Args(               
-              NameExpr(x))))))))
+              NameExpr(x)
+              StrExpr(<30))))))))
 
 [case testFStringWithFormatSpecifierAndConversion]
 x = 'mypy'
@@ -3414,7 +3417,8 @@ MypyFile:1(
               StrExpr({!s:{}})  
               format)           
             Args(               
-              NameExpr(x))))))))
+              NameExpr(x)
+              StrExpr(<30))))))))
 
 [case testFStringWithFormatSpecifierExpression]
 x = 'mypy'
@@ -3441,4 +3445,21 @@ MypyFile:1(
               StrExpr({!s:{}})  
               format)           
             Args(               
-              NameExpr(x))))))))
+              NameExpr(x)
+              CallExpr:3(
+                MemberExpr:3(
+                  StrExpr()
+                  join)
+                Args(
+                  ListExpr:3(
+                    StrExpr(<)
+                    CallExpr:3(
+                      MemberExpr:3(
+                        StrExpr({:{}})
+                        format)
+                      Args(
+                        OpExpr:3(
+                          +
+                          NameExpr(y)
+                          NameExpr(y))
+                        StrExpr()))))))))))))


Code review comments:
diff_hunk: @@ -74,23 +177,406 @@ def __init__(self,
         self.chk = chk
         self.exprchk = exprchk
         self.msg = msg
+        # This flag is used to track Python 2 corner case where for example
+        # '%s, %d' % (u'abc', 42) returns u'abc, 42' (i.e. unicode, not a string).
+        self.unicode_upcast = False
+
+    def check_str_format_call(self, call: CallExpr, format_value: str) -> None:
+        """Perform more precise checks for str.format() calls when possible.
+
+        Currently the checks are performed for:
+          * Actual string literals
+          * Literal types with string values
+          * Final names with string values
+
+        The checks that we currently perform:
+          * Check generic validity (e.g. unmatched { or }, and {} in invalid positions)
+          * Check consistency of specifiers' auto-numbering
+          * Verify that replacements can be found for all conversion specifiers,
+            and all arguments were used
+          * Non-standard format specs are only allowed for types with custom __format__
+          * Type check replacements with accessors applied (if any).
+          * Verify that specifier type is known and matches replacement type
+          * Perform special checks for some specifier types:
+            - 'c' requires a single character string
+            - 's' must not accept bytes
+            - non-empty flags are only allowed for numeric types
+        """
+        conv_specs = self.parse_format_value(format_value, call)
+        if conv_specs is None:
+            return
+        if not self.auto_generate_keys(conv_specs, call):
+            return
+        self.check_specs_in_format_call(call, conv_specs, format_value)
+
+    def parse_format_value(self, format_value: str, ctx: Context,
+                           nested: bool = False) -> Optional[List[ConversionSpecifier]]:
+        """Parse format string into list of conversion specifiers.
+
+        The specifiers may be nested (two levels maximum), in this case they are ordered as
+        '{0:{1}}, {2:{3}{4}}'. Return None in case of an error.
+        """
+        top_targets = self.find_non_escaped_targets(format_value, ctx)
+        if top_targets is None:
+            return None
+
+        result = []  # type: List[ConversionSpecifier]
+        for target in top_targets:
+            match = FORMAT_RE_NEW.fullmatch(target)
+            if match:
+                conv_spec = ConversionSpecifier.from_match(match)
+            else:
+                custom_match = FORMAT_RE_NEW_CUSTOM.fullmatch(target)
+                if custom_match:
+                    conv_spec = ConversionSpecifier.from_match(custom_match,
+                                                               non_standard_spec=True)
+                else:
+                    self.msg.fail('Invalid conversion specifier in format string',
+                                  ctx, code=codes.STRING_FORMATTING)
+                    return None
+
+            if conv_spec.key and ('{' in conv_spec.key or '}' in conv_spec.key):
+                self.msg.fail('Conversion value must not contain { or }',
+                              ctx, code=codes.STRING_FORMATTING)
+                return None
+            result.append(conv_spec)
+
+            # Parse nested conversions that are allowed in format specifier.
+            if (conv_spec.format_spec and conv_spec.non_standard_format_spec and
+                    ('{' in conv_spec.format_spec or '}' in conv_spec.format_spec)):
+                if nested:
+                    self.msg.fail('Formatting nesting must be at most two levels deep',
+                                  ctx, code=codes.STRING_FORMATTING)
+                    return None
+                sub_conv_specs = self.parse_format_value(conv_spec.format_spec[1:], ctx=ctx,
path: mypy/checkstrformat.py
text: @user1:
What is the `[1:]` for?

@author:
The original format spec includes the starting colon, so I remove it everywhere, but here it is indeed unnecessary.

diff_hunk: @@ -74,23 +177,406 @@ def __init__(self,
         self.chk = chk
         self.exprchk = exprchk
         self.msg = msg
+        # This flag is used to track Python 2 corner case where for example
+        # '%s, %d' % (u'abc', 42) returns u'abc, 42' (i.e. unicode, not a string).
+        self.unicode_upcast = False
+
+    def check_str_format_call(self, call: CallExpr, format_value: str) -> None:
+        """Perform more precise checks for str.format() calls when possible.
+
+        Currently the checks are performed for:
+          * Actual string literals
+          * Literal types with string values
+          * Final names with string values
+
+        The checks that we currently perform:
+          * Check generic validity (e.g. unmatched { or }, and {} in invalid positions)
+          * Check consistency of specifiers' auto-numbering
+          * Verify that replacements can be found for all conversion specifiers,
+            and all arguments were used
+          * Non-standard format specs are only allowed for types with custom __format__
+          * Type check replacements with accessors applied (if any).
+          * Verify that specifier type is known and matches replacement type
+          * Perform special checks for some specifier types:
+            - 'c' requires a single character string
+            - 's' must not accept bytes
+            - non-empty flags are only allowed for numeric types
+        """
+        conv_specs = self.parse_format_value(format_value, call)
+        if conv_specs is None:
+            return
+        if not self.auto_generate_keys(conv_specs, call):
+            return
+        self.check_specs_in_format_call(call, conv_specs, format_value)
+
+    def parse_format_value(self, format_value: str, ctx: Context,
+                           nested: bool = False) -> Optional[List[ConversionSpecifier]]:
+        """Parse format string into list of conversion specifiers.
+
+        The specifiers may be nested (two levels maximum), in this case they are ordered as
+        '{0:{1}}, {2:{3}{4}}'. Return None in case of an error.
+        """
+        top_targets = self.find_non_escaped_targets(format_value, ctx)
+        if top_targets is None:
+            return None
+
+        result = []  # type: List[ConversionSpecifier]
+        for target in top_targets:
+            match = FORMAT_RE_NEW.fullmatch(target)
+            if match:
+                conv_spec = ConversionSpecifier.from_match(match)
+            else:
+                custom_match = FORMAT_RE_NEW_CUSTOM.fullmatch(target)
+                if custom_match:
+                    conv_spec = ConversionSpecifier.from_match(custom_match,
+                                                               non_standard_spec=True)
+                else:
+                    self.msg.fail('Invalid conversion specifier in format string',
+                                  ctx, code=codes.STRING_FORMATTING)
+                    return None
+
+            if conv_spec.key and ('{' in conv_spec.key or '}' in conv_spec.key):
+                self.msg.fail('Conversion value must not contain { or }',
+                              ctx, code=codes.STRING_FORMATTING)
+                return None
+            result.append(conv_spec)
+
+            # Parse nested conversions that are allowed in format specifier.
+            if (conv_spec.format_spec and conv_spec.non_standard_format_spec and
+                    ('{' in conv_spec.format_spec or '}' in conv_spec.format_spec)):
+                if nested:
+                    self.msg.fail('Formatting nesting must be at most two levels deep',
+                                  ctx, code=codes.STRING_FORMATTING)
+                    return None
+                sub_conv_specs = self.parse_format_value(conv_spec.format_spec[1:], ctx=ctx,
+                                                         nested=True)
+                if sub_conv_specs is None:
+                    return None
+                result.extend(sub_conv_specs)
+        return result
+
+    def find_non_escaped_targets(self, format_value: str, ctx: Context) -> Optional[List[str]]:
+        """Return list of raw (un-parsed) format specifiers in format string.
+
+        Format specifiers don't include enclosing braces. We don't use regexp for
+        this because they don't work well with nested/repeated patterns
+        (both greedy and non-greedy), and these are heavily used internally for
+        representation of f-strings.
+
+        Return None in case of an error.
+        """
+        result = []
+        next_spec = ''
+        pos = 0
+        nesting = 0
+        while pos < len(format_value):
+            c = format_value[pos]
+            if not nesting:
+                # Skip any paired '{{' and '}}', enter nesting on '{', report error on '}'.
+                if c == '{':
+                    if pos < len(format_value) - 1 and format_value[pos + 1] == '{':
+                        pos += 1
+                    else:
+                        nesting = 1
+                if c == '}':
+                    if pos < len(format_value) - 1 and format_value[pos + 1] == '}':
+                        pos += 1
+                    else:
+                        self.msg.fail('Invalid conversion specifier in format string:'
+                                      ' unexpected }', ctx, code=codes.STRING_FORMATTING)
+                        return None
+            else:
+                # Adjust nesting level, then either continue adding chars or move on.
+                if c == '{':
+                    nesting += 1
+                if c == '}':
+                    nesting -= 1
+                if nesting:
+                    next_spec += c
+                else:
+                    result.append(next_spec)
+                    next_spec = ''
+            pos += 1
+        if nesting:
+            self.msg.fail('Invalid conversion specifier in format string:'
+                          ' unmatched {', ctx, code=codes.STRING_FORMATTING)
+            return None
+        return result
+
+    def check_specs_in_format_call(self, call: CallExpr,
+                                   specs: List[ConversionSpecifier], format_value: str) -> None:
+        """Perform pairwise checks for conversion specifiers vs their replacements.
+
+        The core logic for format checking is implemented in this method.
+        """
+        assert all(s.key for s in specs), "Keys must be auto-generated first!"
+        replacements = self.find_replacements_in_call(call, [cast(str, s.key) for s in specs])
+        assert len(replacements) == len(specs)
+        for spec, repl in zip(specs, replacements):
+            repl = self.apply_field_accessors(spec, repl, ctx=call)
+            actual_type = repl.type if isinstance(repl, TempNode) else self.chk.type_map.get(repl)
+            assert actual_type is not None
+
+            # Special case custom formatting.
+            if (spec.format_spec and spec.non_standard_format_spec and
+                    # Exclude "dynamic" specifiers (i.e. containing nested formatting).
+                    not ('{' in spec.format_spec or '}' in spec.format_spec)):
+                if not custom_special_method(actual_type, '__format__') or spec.conversion:
+                    # TODO: add support for some custom specs like datetime?
+                    self.msg.fail('Unrecognized format'
+                                  ' specification "{}"'.format(spec.format_spec[1:]),
+                                  call, code=codes.STRING_FORMATTING)
+                    continue
+            # Adjust expected and actual types.
+            if not spec.type:
+                expected_type = AnyType(TypeOfAny.special_form)  # type: Optional[Type]
+            else:
+                assert isinstance(call.callee, MemberExpr)
+                if isinstance(call.callee.expr, (StrExpr, UnicodeExpr)):
+                    format_str = call.callee.expr
+                else:
+                    format_str = StrExpr(format_value)
+                expected_type = self.conversion_type(spec.type, call, format_str,
+                                                     format_call=True)
+            if spec.conversion is not None:
+                # If the explicit conversion is given, then explicit conversion is called _first_.
+                if spec.conversion[1] not in 'rsa':
+                    self.msg.fail('Invalid conversion type "{}",'
+                                  ' must be one of "r", "s" or "a"'.format(spec.conversion[1]),
+                                  call, code=codes.STRING_FORMATTING)
+                actual_type = self.named_type('builtins.str')
+
+            # Perform the checks for given types.
+            if expected_type is None:
+                continue
+            self.check_placeholder_type(actual_type, expected_type, call)
+            self.perform_special_format_checks(spec, call, repl, actual_type, expected_type)
+
+    def perform_special_format_checks(self, spec: ConversionSpecifier, call: CallExpr,
+                                      repl: Expression, actual_type: Type,
+                                      expected_type: Type) -> None:
+        # TODO: try refactoring to combine this logic with % formatting.
+        if spec.type == 'c':
+            if isinstance(repl, (StrExpr, BytesExpr)) and len(cast(StrExpr, repl).value) != 1:
path: mypy/checkstrformat.py
text: @user1:
This cast is going to crash with mypyc if it is a `BytesExpr`. (Why is the cast needed?)