22

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.

1
  • You can also use str.format_map() as described in this answer. Commented Apr 13, 2015 at 19:43

4 Answers 4

24

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")
Sign up to request clarification or add additional context in comments.

10 Comments

.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.
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 <style> tags with rulesets in curly braces that you want to leave unmolested.
I tried this but didn't work. I think you meant to use format_map() instead of format()
@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.
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.
|
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

6 Comments

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
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.
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.
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!)
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.
|
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

1 Comment

Thanks. Its userful, but as Fabian mentioned, doesn't handle mapping objects other than dict
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'

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.