41

If I write in Python:

data = {'n': 3, 'k': 3.141594, 'p': {'a': 7, 'b': 8}}
print('{n}, {k:.2f}, {p[a]}, {p[b]}'.format(**data))
del data['k']
data['p']['b'] = None
print('{n}, {k:.2f}, {p[a]}, {p[b]}'.format(**data))

I get:

3, 3.14, 7, 8
Traceback (most recent call last):
  File "./funky.py", line 186, in <module>
    print('{n}, {k:.2f}, {p[a]}, {p[b]}'.format(**data))
KeyError: 'k'

Instead of an error message, how can I get Python to more gracefully format the None's and non existent fields?

To give an example, I would like to see in the output something more like:

3, 3.14, 7, 8
3, ~, 7, ~

Ideally, of course, I would like to be able to specify the string used instead of those missing values.

dreftymac
  • 31,404
  • 26
  • 119
  • 182
Juan A. Navarro
  • 10,595
  • 6
  • 48
  • 52
  • **See also:** [python str.format blank values](https://duckduckgo.com/?q=site%3Astackoverflow.com+python+str.format+blank+value&ia=web) – dreftymac Oct 29 '17 at 09:47

3 Answers3

44

The recommendation in PEP 3101 is to subclass Formatter:

import string
class PartialFormatter(string.Formatter):
    def __init__(self, missing='~~', bad_fmt='!!'):
        self.missing, self.bad_fmt=missing, bad_fmt

    def get_field(self, field_name, args, kwargs):
        # Handle a key not found
        try:
            val=super(PartialFormatter, self).get_field(field_name, args, kwargs)
            # Python 3, 'super().get_field(field_name, args, kwargs)' works
        except (KeyError, AttributeError):
            val=None,field_name 
        return val 

    def format_field(self, value, spec):
        # handle an invalid format
        if value==None: return self.missing
        try:
            return super(PartialFormatter, self).format_field(value, spec)
        except ValueError:
            if self.bad_fmt is not None: return self.bad_fmt   
            else: raise

fmt=PartialFormatter()
data = {'n': 3, 'k': 3.141594, 'p': {'a': '7', 'b': 8}}
print(fmt.format('{n}, {k:.2f}, {p[a]}, {p[b]}', **data))
# 3, 3.14, 7, 8
del data['k']
data['p']['b'] = None
print(fmt.format('{n}, {k:.2f}, {p[a]:.2f}, {p[b]}', **data))
# 3, ~~, !!, ~~

As set up, it will print ~~ if a field or attribute is not found and !! if an invalid format is used given the field value. (Just use None for the keyword argument bad_fmt if you want the default of a value error raised.)

To handle missing keys, you need to subclass both get_field to catch the KeyError or AttributeError and format_field to return a default value for the missing key.

Since you are catching format_field errors, you can catch a bad format field as well by catching the ValueError from the superclass.

dawg
  • 98,345
  • 23
  • 131
  • 206
  • Thanks, I think this version is more robust. I've tweaked it a bit, so that format errors still throw up exceptions, my version is here: https://gist.github.com/navarroj/7689682 – Juan A. Navarro Nov 28 '13 at 10:04
  • 1
    Bug here I think in that the formatters isn't using it's own `self.missing`: `val=None,field_name` vs `val=self.missing,field_name` – royal Dec 12 '18 at 15:18
  • If you look below under `format_field` you see where `None` is handled with `if value==None: return self.missing` – dawg Dec 12 '18 at 15:29
10

If you're able to do the formatting separately you could use Template.safe_substitute which gracefully handles missing values:

>>> from string import Template
>>> t = Template("$a $b $c")
>>> t.safe_substitute(a=3)
'3 $b $c'
Simeon Visser
  • 118,920
  • 18
  • 185
  • 180
  • 1
    I also thought about that. But then how do I replace the non-substituted fields with `~` or some other string? Also, of course, I do want to format floats, etc. – Juan A. Navarro Nov 27 '13 at 17:23
9

The str.format() method doesn't give you a direct method to handle missing keys or replace values.

You can add a layer of indirection; pass in a mapping that handles missing and None values, and alter the format to use just that argument:

class PlaceholderFormatValue():
    def __format__(self, spec):
        return '~'
    def __getitem__(self, name):
        # handle further nested item access
        return self

class formatting_dict(dict):
    def __getitem__(self, name):
        value = self.get(name)
        if isinstance(value, dict):
            # rewrap nested dictionaries to handle missing nested keys
            value = type(self)(value)
        return value if value is not None else PlaceholderFormatValue()

print('{0[n]}, {0[k]:.2f}, {0[p][a]}, {0[p][b]}'.format(formatting_dict(data)))

Now all slots refer to positional argument 0, which is treated like a dictionary, but key lookups always succeed and both missing values and None are replaced by a placeholder value.

Here the PlaceholderFormatValue() ensures that regardless of what the format spec gives, the value can be interpolated into the format. This makes {0[k]:.2f} work, for example.

By wrapping any dict values and having PlaceholderFormatValue handle item access, the above can also handle failure to provide nested keys or whole dictionaries:

>>> data = {'n': 3, 'k': 3.141594, 'p': {'a': 7, 'b': 8}}
>>> del data['k']
>>> data['p']['b'] = None
>>> print('{0[n]}, {0[k]:.2f}, {0[p][a]}, {0[p][b]}'.format(formatting_dict(data)))
3, ~, 7, ~
>>> del data['p']['a']
>>> print('{0[n]}, {0[k]:.2f}, {0[p][a]}, {0[p][b]}'.format(formatting_dict(data)))
3, ~, ~, ~
>>> del data['p']
>>> print('{0[n]}, {0[k]:.2f}, {0[p][a]}, {0[p][b]}'.format(formatting_dict(data)))
3, ~, ~, ~
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    This is nice, but still doesn't quite work. If I remove the `b` key inside of `data['p']` I still get an error (I guess because formatting `0[p]` returned a regular `dict` and not a `formatting_dict`), also if I remove the whole `p` in `data` then it breaks as it's trying to script the non-scriptable `PlaceholderFormatValue`. – Juan A. Navarro Nov 27 '13 at 17:21
  • 1
    @JuanA.Navarro: Right, adjusted for those two cases. – Martijn Pieters Nov 27 '13 at 17:30
  • `The str.format() method doesn't give you a direct method to handle missing keys or replace values.` -- Sure it does. Just [subclass Formatter](http://docs.python.org/2/library/string.html#string.Formatter). – dawg Nov 27 '13 at 18:39
  • @dawg: That is *not* the `str.format()` method, however. That is the `Formatter` class. – Martijn Pieters Nov 27 '13 at 18:53
  • Quoting the docs: 'The Formatter class in the string module allows you to create and customize your own string formatting behaviors **using the same implementation as the built-in format() method.**' Why do you say it is not the same? – dawg Nov 27 '13 at 18:58
  • @dawg: Because you are not calling `str.format()`; you are using a separate class. :-) Which I admit I should have looked at, but it is *not* exactly the same thing. :-) – Martijn Pieters Nov 27 '13 at 19:29
  • @dawg `Formatter` is _Deprecated since version 3.4: Due to lack of usage, the formatter module has been deprecated_. Cited from [Python official docs](https://docs.python.org/3/library/formatter.html) – loko Jun 16 '21 at 14:38