Python String Format vs f string: Which to Use
python string format vs f string: Compare Python string formatting approaches: %-formatting, str.format(), and f-strings. Learn syntax, readability, performance, and w...
When you need to build a string from dynamic values in Python, you have three main options: %-formatting, str.format(), and f-strings. The comparison between python string format vs f string often comes down to readability, flexibility, and performance. This article breaks down each approach and explains when to use which.
The Three String Formatting Approaches in Python
Python offers three distinct ways to interpolate values into strings. The oldest is %-formatting, inherited from C's printf style. It uses %s, %d, %f and similar placeholders. The second is str.format(), introduced in Python 2.6, which uses curly braces {} as placeholders and supports explicit field names and format specifiers. The third is the formatted string literal, or f-string, added in Python 3.6. An f-string is prefixed with f or F and allows arbitrary expressions inside the braces.
Each method has its own syntax and behavior. Understanding the differences helps you write code that is both clear and efficient.
Syntax Comparison: %-formatting, str.format(), and f-strings
The syntax differences are immediately visible in simple examples. Consider formatting a user's name and score.
name = "Alice" score = 92 # %-formatting message = "%s scored %d" % (name, score) # str.format() message = "{} scored {}".format(name, score) # f-string message = f"{name} scored {score}"
The f-string version is the most concise. It places the variables directly inside the string, eliminating the need for a separate argument tuple or method call. This reduces the chance of mismatching placeholders with arguments.
All three support format specifiers for controlling alignment, width, precision, and type. For example, to show the score as a floating-point number with two decimals:
# %-formatting message = "%s scored %.2f" % (name, score) # str.format() message = "{} scored {:.2f}".format(name, score) # f-string message = f"{name} scored {score:.2f}"
The format specifier syntax is nearly identical between str.format() and f-strings, because f-strings reuse the same mini-language. %-formatting uses a different set of specifiers, which can be confusing when switching between styles.
Readability and Maintainability: Why f-strings Win for Most Code
F-strings make the code read naturally from left to right. The expression that produces the value appears exactly where it will be inserted. This is especially useful when the value is not just a simple variable but a function call or an arithmetic operation.
def get_discount(price, rate): return price * rate price = 100 rate = 0.2 # f-string message = f"Final price: {get_discount(price, rate):.2f}" # str.format() message = "Final price: {:.2f}".format(get_discount(price, rate)) # %-formatting message = "Final price: %.2f" % get_discount(price, rate)
With f-strings, you can see the function call and the format specifier in one place. With str.format(), the expression is separated from the placeholder, which makes the code harder to scan when the format string is long. %-formatting has the same problem.
F-strings also support multiline strings and nested quotes more naturally. For example, you can use double quotes inside an f-string that is delimited by single quotes, or vice versa.
message = f"User {name!r} said: 'Hello'"
The !r conversion calls repr() on the value, which is useful for debugging.
When str.format() Is Still Useful: Dynamic Format Strings
F-strings are evaluated at compile time, meaning the format string itself must be a literal in the source code. You cannot construct an f-string dynamically at runtime. If the format template comes from a configuration file, a database, or user input, you must use str.format() or %-formatting.
For example, imagine a logging system where the message template is stored in a dictionary:
templates = { "info": "Process {pid} started", "error": "Process {pid} failed with {error}" } pid = 1234 error = "timeout" message = templates["error"].format(pid=pid, error=error)
Here, the format string is not known until runtime, so an f-string is impossible. str.format() is the right tool because it accepts the template as a string variable.
Another common use case is building a reusable formatter that takes a template as an argument:
def render(template, **kwargs): return template.format(**kwargs) render("Hello {name}", name="Bob")
This pattern is common in web frameworks and templating engines. str.format() also supports indexed fields and attribute access, which can be useful when you have a list of values or an object.
Performance: f-strings Are Faster Because They Are Compiled
F-strings are processed at compile time into a more efficient bytecode representation. The expression inside the braces is evaluated directly, and the string is built using efficient string operations. In contrast, str.format() and %-formatting parse the format string at runtime, which adds overhead.
This does not mean you should obsess over micro-optimizations. For most applications, the difference is negligible. But in tight loops that format thousands of strings per second, f-strings can provide a measurable improvement. The exact numbers depend on the Python version and the complexity of the format string, but the underlying mechanism is clear: f-strings avoid the runtime parsing step.
There is also a memory benefit. F-strings do not create an intermediate format string object that must be parsed; they build the result directly. This reduces allocation pressure.
Common Pitfalls and Edge Cases
F-strings have a few quirks that can trip up developers. One is escaping curly braces. If you need a literal brace in the output, you double it: {{ and }}.
message = f"{{name}} is {name}"
Another issue is using quotes inside the expression. If the f-string is delimited by single quotes, you cannot use single quotes inside the expression without escaping them. You can switch to double quotes for the outer delimiter or use a different quote style inside.
# Valid message = f"{name.upper()} said 'hi'" # Invalid: syntax error message = f'{name.upper()} said 'hi''
F-strings also do not support the = sign for debugging in versions before Python 3.8. In Python 3.8 and later, you can write f"{name=}" to print both the variable name and its value.
When using str.format(), a common mistake is forgetting to escape braces in a template that contains JSON or CSS. You need to double them as well.
Another edge case is handling None values. All three methods convert None to the string "None" by default, but you can override this with a format specifier or a conversion.
Choosing the Right Approach for Your Codebase
For new code, f-strings are almost always the best choice. They are readable, concise, and fast. Use them unless you need a dynamic format string, in which case str.format() is the appropriate alternative.
%-formatting is still relevant in two situations. First, the logging module uses %-formatting by design, and the logging documentation recommends it over f-strings because the logging framework can defer the formatting until the message is actually emitted. Second, you may encounter legacy code that already uses %-formatting; there is no urgent need to rewrite it unless you are touching that code anyway.
When you do need a dynamic template, str.format() is more flexible than %-formatting because it supports named fields, attribute access, and indexing. It also uses the same mini-language as f-strings, so switching between the two is straightforward.
Here is a decision rule: use f-strings for static templates, str.format() for templates that are constructed or loaded at runtime, and %-formatting only for logging or legacy compatibility.
| Method | Syntax | Dynamic template | Readability | Performance |
|---|---|---|---|---|
| %-formatting | "%s %d" % (a, b) | Yes | Low | Medium |
| str.format() | "{} {}".format(a, b) | Yes | Medium | Medium |
| f-string | f"{a} {b}" | No | High | High |
The table summarizes the tradeoffs. F-strings lead in readability and performance but cannot be used when the format string is not known at compile time. str.format() covers that gap with a slight readability cost. %-formatting is the legacy option that remains useful in logging contexts.
Understanding these differences lets you write Python that is both idiomatic and efficient. When you reach for a string formatting method, you now have a clear basis for the choice.