Custom formatter for Python strings

15 Dec '15

This post was inspired by my quest for sane pluralisation.

The string.format method in Python is probably my favourite string formatting mechanism in any language. C# is a close second.

As most people now know, format officially replaced %-based formatting almost 10 years ago now. Do not use % for new projects, ever. If you see %-based formats, either the code base is ancient or the programmer is bad at Python. While it’s unfortunately true that some libraries e.g. logging never caught up, format fixes several issues that % had, and in my opinion is easier and more consistent throughout.

For basic usage, pyformat.info has outstanding documentation. Today, we’re attempting something more advanced: custom formatters.


The documentations don’t make this abundantly clear, but you can extend format. This turns out to be very useful and extremely easy. Two options in particular stand out: format_field and convert_field.

convert_field

This is responsible for e.g. {0!r}. From experimentation, I think conversion type must be a single letter.

The documentation for this method is weak. Only in the next section do we find why you might use this instead of going straight to format_field. It’s sort of obvious when you think about it expressed in this way though:

The conversion field causes a type coercion before formatting. […] Three conversion flags are currently supported: '!s' which calls str() on the value, '!r' which calls repr() and '!a' which calls ascii().

Format String Syntax

So there you have it. The main use case that I see is if you have an object where you want some custom output associated with it, but you don’t want to (ab)use __str__, __repr__, or __format__ or those are all already in use.

import string

class Foo(object):
    def __meaning_of_life__(self):
        return 42

class ProfoundFormatter(string.Formatter):
    def convert_field(self, value, conversion):
        if conversion == 'm':
            return value.__meaning_of_life__()
        else:
            return super().convert_field(value, conversion)

The neat thing is that the coerced value is used for further formatting:

>>> fmt = ProfoundFormatter()
>>> foo = Foo()
>>> fmt.format('{0!m}', foo)
'42'
>>> fmt.format('{0!m:x}', foo)
'2a'
>>> fmt.format('{0!m:4X}', foo)
'  2A'

format_field

This one is more straight forward. The format specification is totally arbitrary and can be as long or complex as desired.

A good example might be for pluralisation. What a coincidence!

import string

class PluralFormatter(string.Formatter):
    def format_field(self, value, format_spec):
        if format_spec.startswith('plural,'):
            words = format_spec.split(',')
            if value == 1:
                return words[1]
            else:
                return words[2]
        else:
            return super().format_field(value, format_spec)
>>> fmt = PluralFormatter()
>>> msg = '{0} {0:plural,bottle,bottles} on the wall'
>>> for bottle_count in (99, 3, 2, 1, 0):
...     print(fmt.format(msg, bottle_count))
99 bottles on the wall
3 bottles on the wall
2 bottles on the wall
1 bottle on the wall
0 bottles on the wall

Python

Newer Older