pythonlistmodulostring-interpolationpython-3.2

Python 'string' % [1, 2, 3] doesn't raise TypeError


Is the exact behavior of the str.__mod__ documented?

These two lines of code works just as expected:

>>> 'My number is: %s.' % 123
'My number is: 123.'
>>> 'My list is: %s.' % [1, 2, 3]
'My list is: [1, 2, 3].'

This line behaves as expected too:

>>> 'Not a format string' % 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: not all arguments converted during string formatting

But what does this line and why it doesn't raise any error?

>>> 'Not a format string' % [1, 2, 3]
'Not a format string'

P. S.

>>> print(sys.version)
3.3.2 (default, Aug 15 2013, 23:43:52) 
[GCC 4.7.3]

Solution

  • When the newest printf-style formatting was added, it seems quite a few little quirks appeared in the % formatting. Today (version 3.8), this is documented here, but was already mentionned as far as version 3.3 here.

    The formatting operations described here exhibit a variety of quirks that lead to a number of common errors (such as failing to display tuples and dictionaries correctly). Using the newer formatted string literals, the str.format() interface, or template strings may help avoid these errors. Each of these alternatives provides their own trade-offs and benefits of simplicity, flexibility, and/or extensibility.

    In this specific case, Python sees a non-tuple value with a __getitem__ method on the right hand side of the % and assumes a format_map has to be done. This is typically done with a dict, but could indeed be done with any objects with a __getitem__ method.

    In particular, a format_map is allowed to ignore unused keys, because you typically do not iterate over items of a mapping to access them.

    >>> "Include those items: %(foo)s %(bar)s" % {"foo": 1, "bar": 2, "ignored": 3}
    'Include those items: 1 2'
    

    Your example is a use of that feature where all keys of your container are ignored.

    >>> "Include no items:" % {"foo": 1, "bar": 2}
    'Include no items:'
    

    If you want further proof of that, check what happens when you use a list as the right-hand side.

    >>> lst = ["foo", "bar", "baz"]
    >>> "Include those items: %(0)s, %(2)s" % lst
    TypeError: list indices must be integers or slices, not str
    

    Python indeed attempts to get lst["0"], unfortunately there is no way to specify that the "0" should be converted to int, so this is doomed to fail with the % syntax.

    Older versions

    For the record, this seems to be a quirk which appeared way before Python 3.0, as I get the same behaviour as far as I can go, despite the documentation starting to mention it only for version 3.3.

    Python 3.0.1+ (unknown, May  5 2020, 09:41:19) 
    [GCC 9.2.0] on linux4
    Type "help", "copyright", "credits" or "license" for more information.
    >>> 'Not a format string' % [1, 2, 3]
    'Not a format string'