You change a log call to an f-string because it reads better:
logger.debug(f"cart for {user_id}: {cart}")
The output looks the same. But Python now builds the whole message before
logger.debug runs, even when debug logging is off. The logger also gets
only the finished text, without the template.
An f-string is the right tool when you want the finished string now. Logging, SQL and translation each need the template and the values separately. Give them that, and use f-strings everywhere else.
Pass log values as arguments
import logging
logger = logging.getLogger(__name__)
def log_payment(payment_id: str, user_id: str) -> None:
logger.info("payment %s failed for user %s", payment_id, user_id)
The logger stores the template and the values in a log record. It joins them only when a handler writes the record out, so a disabled level skips the formatting. The record also keeps the template, so a tool that reads log records can group every "payment failed" message together.
The arguments themselves still run before the call.
logger.debug("payload %s", build_payload()) calls build_payload() even
with debug logging off. If that call is slow, check the level first:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("payload %s", build_payload())
Pass SQL values as parameters
import sqlite3
def user_count(connection: sqlite3.Connection, name: str) -> int:
row = connection.execute(
"SELECT count(*) FROM users WHERE name = ?", (name,)
).fetchone()
return int(row[0])
The ? is a placeholder. The driver sends name to the database as a
value, separate from the SQL. Build the same query with an f-string, and a
name such as x' OR '1'='1 becomes part of the SQL and matches every row.
The name for this attack is SQL injection.
Don't put quotes around ?. A placeholder can stand only for a value, such
as a name or a number. When a table or column name must vary, choose it from
a fixed set of allowed names in your code. Other drivers use other
placeholders, such as %s in psycopg. That %s belongs to the driver, so
pass the values to execute as you do with ?.
Translate the template, then fill it in
from gettext import gettext as _
def greeting(name: str) -> str:
return _("Hello, %(name)s") % {"name": name}
Translation tools collect the literal strings in your code into a catalog.
_() looks up the template in that catalog, and then % fills in the name.
With _(f"Hello, {name}"), the lookup gets "Hello, Ada", which is in no
catalog, so the user sees English. A named placeholder such as %(name)s
also lets a translator move the name within the sentence.
Check your understanding
You set logging.Formatter(style="{"). Can you now write logger.info("hello {}", name)?
Show answer
Hide answer
No. The style setting applies to the formatter's own layout string, such
as "{levelname}: {message}". The logger still joins your message and its
arguments with %. With "hello {}" and one argument, logging prints a
--- Logging error --- traceback instead of your message. Keep %s in log
messages.
Use an f-string for everything else
Error messages, file names, printed output and __repr__ all need the
finished string. Build it with an f-string. In a
__repr__, !r shows string fields with their quotes, so you can see where
a value starts and ends:
class User:
def __init__(self, name: str, events: list[str]) -> None:
self.name = name
self.events = events
def __repr__(self) -> str:
return f"User(name={self.name!r}, events=<{len(self.events)} items>)"
Showing a count instead of the whole list keeps the output short.
Going further
For a subprocess, pass a list of arguments such as ["git", "log", path]
instead of a shell command built with an f-string. Python then starts the
program directly, without a shell, so a path such as x; rm -rf ~ stays
one argument.
typing.LiteralString marks a parameter that accepts only strings built
from literals in your source code. A type checker that enforces it rejects
an f-string made from user input. mypy accepts the annotation but treats it
as plain str, so the f-string passes. Keep passing SQL values as
parameters.
Python 3.14 adds template strings, written t"...", which keep the template
and the values apart. An exercise near the end of this track builds an HTML
renderer and a SQL helper with them.