Lazy arguments save work and keep messages groupable; format attributes add context for free.
Logging methods take a message template and arguments: log.info("user %s logged in from %s", user, ip). The % formatting happens only if the message is actually emitted, so a dropped DEBUG call costs almost nothing. With an f-string, the string is built every time, even when the message is filtered out.
Arguments also keep the template constant ("user %s logged in"), which log aggregators such as Sentry use to group identical events. Use %s, %d, %r, %.2f; the message template itself always uses %-style, regardless of the formatter style.
The line layout is set by a format string of LogRecord attributes:
%(asctime)s,%(levelname)s,%(name)s,%(message)s,%(module)s,%(funcName)s,%(lineno)d,%(process)d,%(threadName)s.
You can use {-style or $-style for the format string by passing style="{" to basicConfig or Formatter: "{asctime} {levelname:<8} {message}". datefmt controls asctime using time.strftime codes.
If a message has placeholders but the arguments don't match, logging doesn't crash your program; it prints a "--- Logging error ---" traceback to stderr and moves on.
Example
import logging, sys
logging.basicConfig(
stream=sys.stdout, level=logging.DEBUG, style="{",
format="{levelname:<7} {funcName}:{lineno} {message}",
)
log = logging.getLogger(__name__)
class Expensive:
def __str__(self):
print(" (str called)")
return "big object"
def handle(user, ms):
log.info("user %s served in %.1f ms", user, ms)
log.setLevel(logging.INFO)
log.debug("state: %s", Expensive()) # never formatted: no '(str called)'
handle("ada", 12.345)
# INFO handle:14 user ada served in 12.3 msRemember
- Pass values as arguments: log.info('x=%s', x), not f-strings
- Formatting is lazy: skipped when the message is filtered out
- Format strings use LogRecord attributes like %(name)s and %(lineno)d
Watch out for
- Using f-strings in hot debug logging, paying formatting costs even when DEBUG is off.