20

Is there a way to use python string.format such that no exception is thrown when an index is missing, instead an empty string is inserted.

result = "i am an {error} example string {error2}".format(hello=2,error2="success")

here,result should be :

"i am an   example string success"

Right now, python throws a keyerror and stops formatting. Is it possible to change this behavior ?

Thanks

Edit:

There exists Template.safe_substitute (even that leaves the pattern intact instead of inserting an empty string) , but couldn't something similar for string.format

The desired behavior would be similar to string substitution in php.

class Formatter(string.Formatter):
  def get_value(self,key,args,kwargs):
    try:
        if hasattr(key,"__mod__"):
            return args[key]
        else:
            return kwargs[key]
    except:
        return ""

This seems to provide the desired behavior.

Code freak
  • 695
  • 2
  • 8
  • 19

4 Answers4

23

The official solution (Python 3 Docs) for strings in format mappings is to subclass the dict class and to define the magic-method __missing__(). This method is called whenever a key is missing, and what it returns is used for the string formatting instead:

class format_dict(dict):
    def __missing__(self, key):
        return "..."

d = format_dict({"foo": "name"})

print("My %(foo)s is %(bar)s" % d) # "My name is ..."

print("My {foo} is {bar}".format(**d)) # "My name is ..."

Edit: the second print() works in Python 3.5.3, but it does not in e.g. 3.7.2: KeyError: 'bar' is raised and I couldn't find a way to catch it.

After some experiments, I found a difference in Python's behavior. In v3.5.3, the calls are __getitem__(self, "foo") which succeeds and __getitem__(self, "bar") which can not find the key "bar", therefore it calls __missing__(self, "bar") to handle the missing key without throwing a KeyError. In v3.7.2, __getattribute__(self, "keys") is called internally. The built-in keys() method is used to return an iterator over the keys, which yields "foo", __getitem__("foo") succeeds, then the iterator is exhausted. For {bar} from the format string there is no key "bar". __getitem__() and hence __missing_() are not called to handle the situation. Instead, the KeyError is thrown. I don't know how one could catch it, if at all.

In Python 3.2+ you should use format_map() instead (also see Python Bug Tracker - Issue 6081):

from collections import defaultdict
    
d = defaultdict(lambda: "...")
d.update({"foo": "name"})

print("My {foo} is {bar}".format_map(d)) # "My name is ..."

If you want to keep the placeholders, you can do:

class Default(dict):
    def __missing__(self, key): 
        return key.join("{}")
    
d = Default({"foo": "name"})

print("My {foo} is {bar}".format_map(d)) # "My name is {bar}"

As you can see, format_map() does call __missing__().

The following appears to be the most compatible solution as it also works in older Python versions including 2.x (I tested v2.7.15):

class Default(dict):
    def __missing__(self, key):
        return key.join("{}")

d = Default({"foo": "name"})

import string
print(string.Formatter().vformat("My {foo} is {bar}", (), d)) # "My name is {bar}"

To keep placeholders as-is including the format spec (e.g. {bar:<15}) the Formatter needs to be subclassed:

import string

class Unformatted:
    def __init__(self, key):
        self.key = key
    def __format__(self, format_spec):
        return "{{{}{}}}".format(self.key, ":" + format_spec if format_spec else "")

class Formatter(string.Formatter):
    def get_value(self, key, args, kwargs):
        if isinstance(key, int):
            try:
                return args[key]
            except IndexError:
                return Unformatted(key)
        else:
            try:
                return kwargs[key]
            except KeyError:
                return Unformatted(key)


f = Formatter()
s1 = f.vformat("My {0} {1} {foo:<10} is {bar:<15}!", ["real"], {"foo": "name"})
s2 = f.vformat(s1, [None, "actual"], {"bar":"Geraldine"})
print(s1) # "My real {1} name       is {bar:<15}!"
print(s2) # "My real actual name       is Geraldine      !"

Note that the placeholder indices are not changed ({1} remains in the string without a {0}), and in order to substitute {1} you need to pass an array with any odd first element and what you want to substitute the remaining placeholder with as second element (e.g. [None, "actual"]).

You can also call the format() method with positional and named arguments:

s1 = f.format("My {0} {1} {foo:<10} is {bar:<15}!", "real", foo="name")
s2 = f.format(s1, None, "actual", bar="Geraldine")
CodeManX
  • 11,159
  • 5
  • 49
  • 70
  • I just tried that and you can do : 'some{thing}'.format(format_dict('your': arguments)) It effectively silence the KeyError and replace the tag by whatever return the __missing__ magic function. – thomas Jul 09 '15 at 14:57
  • 1
    `.format(**format_dict({"foo": "name"}))` can be used too of course. Your snippet will fail however, because of a syntax error. The dict unpacking `**` is mandatory for `.format()`, is does not accept dicts directly. – CodeManX Jul 09 '15 at 15:12
  • 1
    Consider returning `'{' + key + '}'`instead of `...` from `__missing__` for cases where you want to leave text inside braces unchanged if it's not a valid key in the substitution dict. A good example is HTML template substitution where the template contains – Mike Ellis Jul 03 '17 at 16:15
  • 1
    I tried this but didn't work. I think you meant to use format_map() instead of format() – santileortiz Mar 26 '18 at 19:47
  • 1
    @santileortiz I added additional solutions and an explanation why my original answer no longer works in recent Python versions. I also added an example which keeps the placeholders in the string similar to what MikeEllis suggested. – CodeManX Mar 06 '19 at 17:33
  • This loses any special formatting, like if my string is `"{foo:<10}"`. – HoosierDaddy Oct 04 '19 at 17:44
  • @UricSou I disagree, if I run `print("My {foo:<10} is {bar:<10}!".format_map(d))` with `d = Default()` then I get `My {foo}..... is {bar}.....!` (dots representing spaces). `print("My {foo:<10} is {bar:<10}!".format_map(d))` with `d = Default({"foo": "name", "bar": "Geraldine"})` gives me `My name...... is Geraldine.!`. I'm using Python v3.7.4 on Windows 10. – CodeManX Oct 07 '19 at 09:13
  • @CoDEmanX . The issue appears when a 2nd substitution occurs, since you ideally would not drop the `":<10"` formatting details. Concretely, `print("My {foo:<10} is {bar:<10}!".format_map(Default()).format_map({"foo": "name", "bar": "Geraldine"}))` should be the same as `print("My {foo:<10} is {bar:<10}!".format_map({"foo": "name", "bar": "Geraldine"}))`. They are different with this solution. – HoosierDaddy Oct 08 '19 at 12:27
  • @UricSou I see, so you want to round-trip the placeholders with the format_spec. I studied the standard lib implementation of the Formatter class a bit and added a possible solution to my answer. It might be desired to update the indices of placeholder, but one would have to re-implement pretty much the whole class to achieve that I'm afraid. Your example with named placeholders should work fine however. – CodeManX Oct 10 '19 at 10:24
  • 1
    I think you could simplify this very nice solution quite a bit, but renaming `Unformatted.format` to `Unformatted.__format__`. Then, you can remove `Formatter.format_field` completely as `Unformatted` is treated correctly by the original `string.Formatter.format_field`. Similarly, `vformat` can go since it does nothing but forward arguments to the super class equivalent. – NichtJens Dec 10 '21 at 11:55
20

str.format() doesn't expect a mapping object. Try this:

from collections import defaultdict

d = defaultdict(str)
d['error2'] = "success"
s = "i am an {0[error]} example string {0[error2]}"
print s.format(d)

You make a defaultdict with a str() factory that returns "". Then you make one key for the defaultdict. In the format string, you access keys of the first object passed. This has the advantage of allowing you to pass other keys and values, as long as your defaultdict is the first argument to format().

Also, see http://bugs.python.org/issue6081

Fabian Fagerholm
  • 4,099
  • 1
  • 35
  • 45
  • Could this be improved by using a formatter object (idea frm the bug report link) instead of a defaultdict ? that way, no change wud be reqd to the format variable. Thanks – Code freak Aug 21 '10 at 05:18
  • class Formatter(string.Formatter): def get_value(self,key,args,kwargs): try: if hasattr(key,"__mod__"): return args[key] else: return kwargs[key] except: return "" This works for me. The advantage is that there's no need to create an extra defaultdict. – Code freak Aug 21 '10 at 05:33
  • Yeah, that works well. I'm not sure how to compare the penalty of an extra defaultdict against the penalty of an extra Formatter class+object, but in some situations it might be better, especially if it makes the code clearer. – Fabian Fagerholm Aug 22 '10 at 05:00
  • 1
    To return an arbitrary string, you may use a lambda expression, because defaultdict requires a callable: `"Hello {name}!".format(**defaultdict(lambda: "World"))` (Result: `Hello World!`) – CodeManX Jul 09 '15 at 15:17
  • I am not sure if `defaultdict` has ever worked as a keyword args, but in Python 3.8, the latest suggestion by @CodeManX does not work. – Jérémie Jan 04 '21 at 05:20
  • There was definitely a change between Python versions 3.5 and 3.7. I changed [my answer](https://stackoverflow.com/a/21754294/2044940) because of that in 2019. Above comment from 2015 should probably be ignored. @Jérémie – CodeManX Jan 11 '21 at 13:07
5

Unfortunately, no, there is no such way to do by default. However you can provide it defaultdict or object with overridden __getattr__, and use like this:

class SafeFormat(object):
    def __init__(self, **kw):
        self.__dict = kw

    def __getattr__(self, name):
        if not name.startswith('__'):
            return self.__dict.get(name, '')

print "i am an {0.error} example string {0.error2}".format(SafeFormat(hello=2,error2="success"))
i am an  example string success
Daniel Kluev
  • 11,025
  • 2
  • 36
  • 36
3

I made a version that does work similarly to Daniel's method but without the {0.x} attribute access.

import string    
class SafeFormat(object):
    def __init__(self, **kw):
        self.__dict = kw

    def __getitem__(self, name):
        return self.__dict.get(name, '{%s}' % name)



string.Formatter().vformat('{what} {man}', [], SafeFormat(man=2))

prints out

'{what} 2'
ubershmekel
  • 11,864
  • 10
  • 72
  • 89