I've been there a while ago, and also was confused. And at least a couple more developers felt the same, as there's bpo14031.
Summary
Summary is provided in Python documentation in Use of alternative formatting styles (from the BPO).
When logging was added to the Python standard library, the only way of formatting messages with variable content was to use the %-formatting method.
[...]
Note that the formatting of logging messages for final output to logs is completely independent of how an individual logging message is constructed.
[...]
[...] you cannot directly make logging calls using str.format() or string.Template syntax, because internally the logging package uses %-formatting to merge the format string and the variable arguments. There would be no changing this while preserving backward compatibility, since all logging calls which are out there in existing code will be using %-format strings.
Translating this into Python would mean this.
In [1]: import logging
In [2]: # this is roughly how result of "logging.info('foo %s', 'bar')" looks
...: dummy = logging.makeLogRecord({'msg': 'foo %s', 'args': ('bar',)})
In [3]: # logger.Formatter roughly works as follows
...: dummy.message = dummy.getMessage() # ①
...: '%(message)s' % dummy.__dict__ # ②
Out[3]: 'foo bar'
In [4]: '{message}'.format(**dummy.__dict__) # ③
Out[4]: 'foo bar'
② and ③ are configurable (there's also string.Template option), ① is not and all it is is:
def getMessage(self):
"""
Return the message for this LogRecord.
Return the message for this LogRecord after merging any user-supplied
arguments with the message.
"""
msg = str(self.msg)
if self.args:
msg = msg % self.args
return msg
And sure, since Python 3.2 log record factory is configurable via logging.setLogRecordFactory as described in Customizing LogRecord. But sub-classing it, overriding getMessage and replacing msg = msg % self.args with msg = msg.format(self.args)* will break all existing code using logging and passing arguments (likely in subtle ways depending on the logging level).
Solution
I was thinking how to override getMessage to make is backward-compatible with %-style, support {-style and also not become too slow. Here's what I've come to.
import collections.abc
import logging
class StrFormatLogRecord(logging.LogRecord):
"""
Drop-in replacement for ``LogRecord`` that supports ``str.format``.
"""
def getMessage(self):
msg = str(self.msg)
if self.args:
try:
msg = msg % ()
except TypeError:
# Either or the two is expected indicating there's
# a placeholder to interpolate:
#
# - not all arguments converted during string formatting
# - format requires a mapping" expected
#
# If would've been easier if Python printf-style behaved
# consistently for "'' % (1,)" and "'' % {'foo': 1}". But
# it raises TypeError only for the former case.
msg = msg % self.args
else:
# There's special case of first mapping argument. See
# duner init of logging.LogRecord.
if isinstance(self.args, collections.abc.Mapping):
msg = msg.format(**self.args)
else:
msg = msg.format(*self.args)
return msg
And it can be used like:
def main():
args = {'arg1': 10, 'arg2': 20}
logging.info('Input args: %.2f %.2f', *args.values())
logging.info('Input args: %d %d', *args.values())
logging.info('Input args: %s %s', *args.values())
logging.info('Input args: %(arg1).2f %(arg2).2f', args)
logging.info('Input args: %(arg1)d %(arg2)d', args)
logging.info('Input args: %(arg1)s %(arg2)s', args)
logging.info('Input args: {0:.2f} {1:.2f}', *args.values())
logging.info('Input args: {0:d} {1:d}', *args.values())
logging.info('Input args: {0} {1}', *args.values())
logging.info('Input args: {arg1:.2f} {arg2:.2f}', args)
logging.info('Input args: {arg1:d} {arg2:d}', args)
logging.info('Input args: {arg1} {arg2}', args)
if __name__ == '__main__':
logging.basicConfig(
level=logging.INFO,
format='{asctime} {message}',
style='{',
)
logging.setLogRecordFactory(StrFormatLogRecord)
main()
In all 4 cases it produces correctly formatted output. But it's not unlikely that there's still a couple of edge-cases where it breaks or formats incorrectly (and that's besides the case when a str.format template contains a printf-style placeholder, but it's up to you whether it's relevant).
... Input args: 10.00 20.00
... Input args: 10 20
... Input args: 10 20