65

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
martineau
  • 99,260
  • 22
  • 139
  • 249
nelsonvarela
  • 2,140
  • 6
  • 24
  • 41
  • Which one do you want `bond, bond` / `bond, {james}, bond`? – falsetru Jun 20 '13 at 14:06
  • The second one is better I think. First one can create weird content.. second one makes people think "hey, something is wrong over here" which in this case in a good thing – nelsonvarela Jun 20 '13 at 14:22
  • I updated the answer for both case. – falsetru Jun 20 '13 at 14:24
  • https://stackoverflow.com/questions/5466451/how-can-i-print-literal-curly-brace-characters-in-python-string-and-also-use-fo – Qlimax Aug 30 '17 at 13:02
  • **See also:** https://stackoverflow.com/questions/35574349/python-format-string-with-custom-delimiters – dreftymac Jul 01 '18 at 15:07
  • Interesting blog post about this issue: [Handling missing keys in str.format_map properly](http://ashwch.github.io/handling-missing-keys-in-str-format-map.html) – Delgan May 10 '19 at 16:35

9 Answers9

105

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'
falsetru
  • 314,667
  • 49
  • 610
  • 551
26

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'})
Martin Maillard
  • 2,443
  • 16
  • 22
  • I already know this trick years ago, but this entire Q&A still reconfirms that `Template` is the best in this scenario. Upvoted. :-) – RayLuo Feb 16 '21 at 09:23
  • The simplest method by far, and doesn't lead you into the trap of thinking it can handle more complex formatters like `{my_float:>5.2f}`. It's clearly plain old string substitution. – BallpointBen Feb 23 '21 at 21:38
15

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+

psampa
  • 3
  • 3
dawg
  • 80,841
  • 17
  • 117
  • 187
  • 1
    Out of the answers provided here I think this is the best in terms of portability/elegance +1 – Ajay Aug 09 '16 at 14:35
  • I think `Formatter.get_value(key, args, kwds)` **should be** `return string.Formatter.get_value(self, key, args, kwds)` in your code – Grijesh Chauhan Mar 21 '18 at 12:31
  • @GrijeshChauhan I’m not sure… The function is called recursively, the only real return takes place in the last, terminal, call so… Anyway I did not manage to get it working. I finished with a simpler code, which do what I want. class URLFormatter(string.Formatter): def __init__(self, default='{}'): self.default=default def get_value(self, key, args, kwds): return kwds.get(key, self.default.format(key)) – Stéphane Jun 22 '18 at 23:16
9

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.

Community
  • 1
  • 1
goodmami
  • 807
  • 11
  • 23
9

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

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

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.

Ioannis Filippidis
  • 8,272
  • 8
  • 66
  • 96
3

Needing to partially fill format strings is a common problem when progressively filling the format strings, e.g. for SQL queries.

format_partial() method uses the Formatter from string and ast to parse the format string and also find out whether the named parameter hash has all the values needed to partially evaluate the format:

import ast
from collections import defaultdict
from itertools import chain, ifilter, imap
from operator import itemgetter
import re
from string import Formatter

def format_partial(fstr, **kwargs):
    def can_resolve(expr, **kwargs):
        walk = chain.from_iterable(imap(ast.iter_fields, ast.walk(ast.parse(expr))))
        return all(v in kwargs for k,v in ifilter(lambda (k,v): k=='id', walk))

    ostr = fstr
    fmtr = Formatter()
    dd = defaultdict(int)
    fmtr.get_field = lambda field_name, args, kwargs: (dd[field_name],field_name)
    fmtr.check_unused_args = lambda used_args, args, kwargs: all(v in dd for v in used_args)
    for t in ifilter(itemgetter(1), Formatter().parse(fstr)):
        f = '{'+t[1]+(':'+t[2] if t[2] else '')+'}'
        dd = defaultdict(int)
        fmtr.format(f,**kwargs)
        if all(can_resolve(e,**kwargs) for e in dd):
            ostr = re.sub(re.escape(f),Formatter().format(f, **kwargs),ostr,count=1)
    return ostr

format_partial will leave the unresolved portion of the format string, so subsequent calls can be used to resolve those parts as the data is available.

goodmami's and dawg's answers seem cleaner, but they both fail to capture the format mini-language completely as in {x:>{x}}; format_partial will have no problem resolving any format string that string.format() resolves:

from datetime import date
format_partial('{x} {} {y[1]:x} {x:>{x}} {z.year}', **{'x':30, 'y':[1,2], 'z':date.today()})

'30 {} 2                             30 2016'

It is even easier to extend the functionality to old style format strings using regex instead of the string formatter, as the old style format substrings were regular (ie. no nested markers).

topkara
  • 836
  • 9
  • 15
2

Here's another way to do it using python27:

action = '{bond}, {james} {bond}'
d = dict((x[1], '') for x in action._formatter_parser())
# Now we have: `d = {'james': '', 'bond': ''}`.
d.update(bond='bond')
print action.format(**d)  # bond,  bond
feqwix
  • 1,058
  • 11
  • 14
  • 1
    Some may balk at the usage of `_formatter_parser`, but to me this is the most pythonic approach: simple, easy to comprehend, uses out-of-the-box functionality, and if you modify the second line to `d = dict((x[1], '{'+str(x[1])+'}') for x in action._formatter_parser())` you can get the `bond, {james} bond` format just as easily as the `bond, bond` format. – hlongmore Mar 14 '18 at 05:03
1

For Python 3, taking the approved answer, this is a nice, tight, Pythonic implementation:

def safeformat(str, **kwargs):
    class SafeDict(dict):
        def __missing__(self, key):
            return '{' + key + '}'
    replacements = SafeDict(**kwargs)
    return str.format_map(replacements)

# In [1]: safeformat("a: {a}, b: {b}, c: {c}", a="A", c="C", d="D")
# Out[1]: 'a: A, b: {b}, c: C'
mattmc3
  • 15,986
  • 5
  • 75
  • 96
1

Based on some of the other answers, I expanded the solutions. This will handle strings with formatting spec "{a:<10}".

I found that some strings from selenium logging were causing vformat (and format_map) to hit a recursion limit. I also wanted to ensure I could handle strings where empty curly braces exist as well.

def partialformat(s: str, recursionlimit: int = 10, **kwargs):
    """
    vformat does the acutal work of formatting strings. _vformat is the 
    internal call to vformat and has the ability to alter the recursion 
    limit of how many embedded curly braces to handle. But for some reason 
    vformat does not.  vformat also sets the limit to 2!   

    The 2nd argument of _vformat 'args' allows us to pass in a string which 
    contains an empty curly brace set and ignore them.
    """

    class FormatPlaceholder:
        def __init__(self, key):
            self.key = key

        def __format__(self, spec):
            result = self.key
            if spec:
                result += ":" + spec
            return "{" + result + "}"

    class FormatDict(dict):
        def __missing__(self, key):
            return FormatPlaceholder(key)

    class PartialFormatter(string.Formatter):
        def get_field(self, field_name, args, kwargs):
            try:
                obj, first = super(PartialFormatter, self).get_field(field_name, args, kwargs)
            except (IndexError, KeyError, AttributeError):
                first, rest = formatter_field_name_split(field_name)
                obj = '{' + field_name + '}'

                # loop through the rest of the field_name, doing
                #  getattr or getitem as needed
                for is_attr, i in rest:
                    if is_attr:
                        try:
                            obj = getattr(obj, i)
                        except AttributeError as exc:
                            pass
                    else:
                        obj = obj[i]

            return obj, first

    fmttr = string.Formatter()
    fs, _ = fmttr._vformat(s, ("{}",), FormatDict(**kwargs), set(), recursionlimit)
    return fs

class ColorObj(object):
    blue = "^BLUE^"
s = '{"a": {"b": {"c": {"d" : {} {foo:<12} & {foo!r} {arg} {color.blue:<10} {color.pink} {blah.atr} }}}}'
print(partialformat(s, foo="Fooolery", arg="ARRrrrrrg!", color=ColorObj))

output:

{"a": {"b": {"c": {"d" : {} Fooolery             & 'Fooolery' Fooolery ARRrrrrrg! ^BLUE^ {color.pink} {blah.atr} }}}}
Marcel Wilson
  • 2,803
  • 20
  • 43