Basics of String Formatting in Python: Complete f-Strings, format(), and % Formatting Guide

Basics of String Formatting in python

Basics of String Formatting in python

I’ve written Python code across more than one era of its string formatting history, and honestly, having three different formatting systems available at once used to confuse me more than help. Once I understood the history — why % formatting came first, why .format() replaced it, and why f-strings eventually became the clear winner — everything clicked. This guide walks through all three approaches, when I’d still reach for the older ones, and the internal mechanics behind why f-strings are as fast as they are.

Method 1: Old-Style % Formatting

This is Python’s original string formatting mechanism, borrowed conceptually from C’s printf.

name = "Alice"
age = 30
print("My name is %s and I am %d years old." % (name, age))
# Output: My name is Alice and I am 30 years old.

Common format specifiers:

print("%s" % "text")     # String
print("%d" % 42)         # Integer
print("%f" % 3.14159)    # Float — Output: 3.141590
print("%.2f" % 3.14159)  # Float with 2 decimal places — Output: 3.14
print("%x" % 255)        # Hexadecimal — Output: ff

I still see % formatting in older codebases and in Python’s own logging module conventions (logging.info("Value: %s", value)), but I rarely choose it for new code — it’s less readable with multiple values, and it doesn’t handle missing arguments gracefully.

Method 2: str.format()

Introduced in Python 2.6/3.0, .format() was designed to fix many of %‘s shortcomings.

name = "Alice"
age = 30
print("My name is {} and I am {} years old.".format(name, age))
# Output: My name is Alice and I am 30 years old.

Positional and Named Arguments

print("{0} is {1} years old. {0} likes Python.".format("Alice", 30))
# Output: Alice is 30 years old. Alice likes Python.

print("{name} is {age} years old.".format(name="Alice", age=30))
# Output: Alice is 30 years old.

Format Specifications

print("{:.2f}".format(3.14159))     # Output: 3.14
print("{:>10}".format("hi"))        # Right-align in 10 chars — Output: '        hi'
print("{:<10}|".format("hi"))       # Left-align — Output: 'hi        |'
print("{:^10}|".format("hi"))       # Center-align — Output: '    hi    |'
print("{:,}".format(1000000))       # Thousands separator — Output: 1,000,000
print("{:.1%}".format(0.256))       # Percentage — Output: 25.6%

.format() was a genuine improvement — I could reorder, reuse, and name arguments, and format specifiers became far more expressive. It’s still a solid choice, especially when the format string itself is built dynamically or comes from an external source like a config file or translation table, since f-strings can’t be constructed that way (they require the expressions to be literally embedded at the point of definition).

Method 3: f-Strings (Formatted String Literals) — My Default Choice

Introduced in Python 3.6 via PEP 498, f-strings let me embed expressions directly inside string literals, prefixed with f.

name = "Alice"
age = 30
print(f"My name is {name} and I am {age} years old.")
# Output: My name is Alice and I am 30 years old.

The biggest advantage: I can put any valid Python expression inside the braces, not just variable names.

x, y = 5, 10
print(f"The sum of {x} and {y} is {x + y}.")
# Output: The sum of 5 and 10 is 15.

items = ["apple", "banana"]
print(f"First item: {items[0].upper()}")
# Output: First item: APPLE

Format Specs Inside f-Strings

f-strings support the exact same format spec mini-language as .format():

pi = 3.14159265
print(f"{pi:.2f}")       # Output: 3.14
print(f"{1000000:,}")    # Output: 1,000,000
print(f"{0.256:.1%}")    # Output: 25.6%

The = Debugging Specifier (Python 3.8+)

One of my favorite additions — this prints both the expression and its value, which I use constantly for quick debugging instead of writing print(f"x: {x}") by hand.

x = 42
print(f"{x=}")
# Output: x=42

name = "Alice"
print(f"{name.upper()=}")
# Output: name.upper()='ALICE'

Nested Quotes and Multi-Line f-Strings

name = "Alice"
print(f"She said, \"Hello, {name}!\"")
# Output: She said, "Hello, Alice!"

# Python 3.12+ allows the same quote character inside f-strings without escaping
print(f"She said, "Hello, {name}!"")

Why f-Strings Are Faster Internally

This is the part I found genuinely interesting once I looked into it. Unlike % formatting and .format(), which are resolved at runtime by parsing the format string and substituting values through method calls, f-strings are compiled directly into bytecode at parse time. The Python compiler essentially breaks the f-string into a sequence of string-building operations — evaluating each embedded expression and concatenating the pieces — baked directly into the bytecode of the function, rather than being interpreted through a separate formatting function call at runtime.

import dis

name = "Alice"
def greet():
    return f"Hello, {name}!"

dis.dis(greet)

Looking at the disassembly shows the f-string compiled into LOAD_GLOBAL, FORMAT_VALUE, and BUILD_STRING opcodes — direct bytecode instructions — rather than a runtime call into str.format()‘s more general-purpose parsing machinery. This is a big part of why f-strings consistently benchmark faster than both % formatting and .format().

import timeit

name = "Alice"
age = 30

def percent_style():
    return "%s is %d" % (name, age)

def format_style():
    return "{} is {}".format(name, age)

def fstring_style():
    return f"{name} is {age}"

print(timeit.timeit(percent_style, number=1000000))
print(timeit.timeit(format_style, number=1000000))
print(timeit.timeit(fstring_style, number=1000000))

When I ran this myself, f-strings consistently came out fastest, followed by % formatting, with .format() typically the slowest of the three due to its more general-purpose method resolution overhead.

When I Still Use .format() Instead of f-Strings

f-strings can’t be built dynamically from a variable format string — the expressions must be literally present in the source code at the point where the string is written. When the format template itself comes from a configuration file, database, or translation system, .format() is still the right tool:

template = get_template_from_config()  # e.g. "Hello, {name}!"
result = template.format(name="Alice")

This is a case where f-strings simply can’t help, since f"{template}" would just insert the literal template string, not evaluate {name} within it.

Common Mistakes I’ve Made or Seen

# Harder to read
print(f"{'Yes' if user.is_active and user.has_paid and not user.is_banned else 'No'}")

# Cleaner
status = "Yes" if (user.is_active and user.has_paid and not user.is_banned) else "No"
print(f"{status}")

Real-World Applications

FAQs

Q: Should I always use f-strings over .format() and %? For new code with literal, hard-coded templates, yes — f-strings are faster, more readable, and the current recommended standard. Use .format() when the template itself is dynamic or comes from an external source.

Q: Are f-strings actually faster, or does it not matter in practice? They are measurably faster due to compile-time bytecode generation rather than runtime parsing, though for most everyday scripts the difference won’t be noticeable — it matters more in performance-sensitive, high-frequency code paths.

Q: Can I call functions inside an f-string? Yes — any valid Python expression is allowed inside the curly braces, including function and method calls.

Q: Why is % formatting still used anywhere? It persists mostly in legacy code and in specific APIs like Python’s logging module, where deferred, lazy formatting (only evaluated if the log message is actually emitted) is a deliberate performance feature.

Summary

Python’s three string formatting systems — % formatting, .format(), and f-strings — each reflect a different era of the language’s evolution. f-strings are my default for virtually all new code thanks to their readability and compile-time performance advantage, while .format() still earns its place when format templates need to be dynamic or externally sourced. Understanding how each one works under the hood, rather than just memorizing syntax, made switching between them feel natural instead of arbitrary.

References

Exit mobile version