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 callsstr()
on the value,'!r'
which callsrepr()
and'!a'
which callsascii()
.
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