Почему нежелательно использовать f-string при логировании в Python
Что проверяют
Знаете ли вы, как устроен модуль logging, и понимаете ли смысл отложенного форматирования. Вопрос простой, но ответ быстро показывает, писал ли человек что-то, кроме print.
Как отвечать
Главный довод один: f-строка собирается до вызова логгера. Запись уровня DEBUG при отключённом уровне будет отброшена, но работа по склейке и вызов __str__ у аргументов уже выполнены. На горячем пути это заметно.
Ленивый вариант передаёт шаблон и аргументы отдельно:
logger.debug("user=%s order=%s", user_id, order_id)Склейка произойдёт внутри record.getMessage() и только если запись реально дойдёт до обработчика.
Второй довод сильнее первого, и называют его реже: при таком вызове в записи остаются и шаблон, и аргументы по отдельности. Шаблон это стабильный ключ, по которому система сбора логов группирует одинаковые события и считает их частоту. С f-строкой каждое сообщение уникально, и группировка рассыпается.
Полезно добавить, что это ловят линтеры: правило про интерполяцию в логировании есть в pylint и в наборе flake8-logging-format, который в ruff включается группой G. И что стиль с фигурными скобками в самом вызове модуль не поддерживает: сообщение всегда форматируется оператором %, а стиль в Formatter влияет только на строку вывода.
Где спотыкаются
Объясняют запрет тем, что f-строки медленные. Наоборот, это самый быстрый способ форматирования в языке. Проблема не в цене операции, а в том, что она выполняется всегда, в том числе впустую.
Приплетают инъекции и говорят, что через f-строку злоумышленник выполнит свой код. Опасен разбор шаблона из пользовательского ввода через str.format, а f-строка компилируется вместе с кодом, подставить в неё чужой шаблон нельзя. Настоящий риск в логах это персональные данные и секреты внутри аргументов.
Думают, что %s делает ленивым всё. Вызов logger.debug("%s", build_dump(obj)) выполнит build_dump в любом случае: откладывается склейка, а не вычисление аргумента. Дорогую подготовку прячут за logger.isEnabledFor или за объект, который считает себя в __str__.