Format string unused named arguments

34,132

Solution 1

If you are using Python 3.2+, use can use str.format_map().

For bond, bond:

>>> from collections import defaultdict
>>> '{bond}, {james} {bond}'.format_map(defaultdict(str, bond='bond'))
'bond,  bond'

For bond, {james} bond:

>>> class SafeDict(dict):
...     def __missing__(self, key):
...         return '{' + key + '}'
...
>>> '{bond}, {james} {bond}'.format_map(SafeDict(bond='bond'))
'bond, {james} bond'

In Python 2.6/2.7

For bond, bond:

>>> from collections import defaultdict
>>> import string
>>> string.Formatter().vformat('{bond}, {james} {bond}', (), defaultdict(str, bond='bond'))
'bond,  bond'

For bond, {james} bond:

>>> from collections import defaultdict
>>> import string
>>>
>>> class SafeDict(dict):
...     def __missing__(self, key):
...         return '{' + key + '}'
...
>>> string.Formatter().vformat('{bond}, {james} {bond}', (), SafeDict(bond='bond'))
'bond, {james} bond'

Solution 2

You could use a template string with the safe_substitute method.

from string import Template

tpl = Template('$bond, $james $bond')
action = tpl.safe_substitute({'bond': 'bond'})

Solution 3

You can follow the recommendation in PEP 3101 and subclass Formatter:

from __future__ import print_function
import string

class MyFormatter(string.Formatter):
    def __init__(self, default='{{{0}}}'):
        self.default=default

    def get_value(self, key, args, kwds):
        if isinstance(key, str):
            return kwds.get(key, self.default.format(key))
        else:
            return string.Formatter.get_value(key, args, kwds)

Now try it:

>>> fmt=MyFormatter()
>>> fmt.format("{bond}, {james} {bond}", bond='bond', james='james')
'bond, james bond'
>>> fmt.format("{bond}, {james} {bond}", bond='bond')
'bond, {james} bond'

You can change how key errors are flagged by changing the text in self.default to what you would like to show for KeyErrors:

>>> fmt=MyFormatter('">>{{{0}}} KeyError<<"')
>>> fmt.format("{bond}, {james} {bond}", bond='bond', james='james')
'bond, james bond'
>>> fmt.format("{bond}, {james} {bond}", bond='bond')
'bond, ">>{james} KeyError<<" bond'

The code works unchanged on Python 2.6, 2.7, and 3.0+

Solution 4

One can also do the simple and readable, albeit somewhat silly:

'{bar}, {fro} {bar}'.format(bar='bar', fro='{fro}')

I know that this answer requires knowledge of the expected keys, but I was looking for a simple two-step substitution (say problem name first, then problem index within a loop) and creating a whole class or unreadable code was more complex than needed.

Solution 5

falsetru's answer has a clever use of a defaulting dictionary with vformat(), and dawg's answer is perhaps more in-line with Python's documentation, but neither handle compound field names (e.g., with explicit conversion (!r) or format specs (:+10g).

For example, using falsetru's SafeDict:

>>> string.Formatter().vformat('{one} {one:x} {one:10f} {two!r} {two[0]}', (), SafeDict(one=215, two=['James', 'Bond']))
"215 d7 215.000000 ['James', 'Bond'] James"
>>> string.Formatter().vformat('{one} {one:x} {one:10f} {two!r} {two[0]}', (), SafeDict(one=215))
"215 d7 215.000000 '{two}' {"

And using dawg's MyFormatter:

>>> MyFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215, two=['James', 'Bond'])
"215 d7 215.000000 ['James', 'Bond'] James"
>>> MyFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215)
"215 d7 215.000000 '{two}' {"

Neither work well in the second case because the value lookup (in get_value()) has already stripped out the formatting specifications. Instead, you can redefine vformat() or parse() so these specifications are available. My solution below does this by redefining vformat() so it performs the key lookup and, if the key is missing, escapes the format string with double braces (e.g. {{two!r}}) and then performs the normal vformat().

class SafeFormatter(string.Formatter):
    def vformat(self, format_string, args, kwargs):
        args_len = len(args)  # for checking IndexError
        tokens = []
        for (lit, name, spec, conv) in self.parse(format_string):
            # re-escape braces that parse() unescaped
            lit = lit.replace('{', '{{').replace('}', '}}')
            # only lit is non-None at the end of the string
            if name is None:
                tokens.append(lit)
            else:
                # but conv and spec are None if unused
                conv = '!' + conv if conv else ''
                spec = ':' + spec if spec else ''
                # name includes indexing ([blah]) and attributes (.blah)
                # so get just the first part
                fp = name.split('[')[0].split('.')[0]
                # treat as normal if fp is empty (an implicit
                # positional arg), a digit (an explicit positional
                # arg) or if it is in kwargs
                if not fp or fp.isdigit() or fp in kwargs:
                    tokens.extend([lit, '{', name, conv, spec, '}'])
                # otherwise escape the braces
                else:
                    tokens.extend([lit, '{{', name, conv, spec, '}}'])
        format_string = ''.join(tokens)  # put the string back together
        # finally call the default formatter
        return string.Formatter.vformat(self, format_string, args, kwargs)

Here's it in action:

>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215, two=['James', 'Bond'])
"215 d7 215.000000 ['James', 'Bond'] James"
>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', one=215)
'215 d7 215.000000 {two!r} {two[0]}'
>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}')
'{one} {one:x} {one:10f} {two!r} {two[0]}'
>>> SafeFormatter().format('{one} {one:x} {one:10f} {two!r} {two[0]}', two=['James', 'Bond'])
"{one} {one:x} {one:10f} ['James', 'Bond'] James"

This solution is a bit too hacky (maybe redefining parse() would have fewer kludges), but should work for more formatting strings.

Share:
34,132
nelsonvarela
Author by

nelsonvarela

Updated on September 19, 2021

Comments

  • nelsonvarela
    nelsonvarela over 2 years

    Let's say I have:

    action = '{bond}, {james} {bond}'.format(bond='bond', james='james')
    

    this wil output:

    'bond, james bond' 
    

    Next we have:

     action = '{bond}, {james} {bond}'.format(bond='bond')
    

    this will output:

    KeyError: 'james'
    

    Is there some workaround to prevent this error to happen, something like:

    • if keyrror: ignore, leave it alone (but do parse others)
    • compare format string with available named arguments, if missing then add