Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

Python String Formatting: f-strings, format(), and When Each One Wins

Python has three mature string formatting systems and one more niche one. They’ve accumulated over the language’s history, and each still has valid use cases. The trick is knowing which to reach for when — and knowing the format specification mini-language that makes all of them more powerful.


f-strings (Python 3.6+)

F-strings are the modern standard. Prefix a string with f or F, and anything inside curly braces is evaluated as a Python expression:

name = "Alice"
age = 30
score = 94.333
print(f"Name: {name}")
print(f"Age next year: {age + 1}")
print(f"Score: {score:.2f}") # 94.33 — two decimal places
print(f"Name: {name!r}") # 'Alice' — repr(), shows quotes
print(f"Name: {name!u}") # ALICE — .upper() shorthand? No — use expressions:
print(f"Name: {name.upper()}") # ALICE — expressions work

f-string debugging (Python 3.8+)

Add = after the expression to print the expression and its value — invaluable for debugging:

x = 42
items = [1, 2, 3]
print(f"{x = }") # x = 42
print(f"{len(items) = }") # len(items) = 3

Format spec mini-language

The colon inside f-string braces introduces a format specification:

n = 1234567.89
# Number formatting
print(f"{n:,.2f}") # 1,234,567.89 — comma separator, 2 decimal places
print(f"{n:.0f}") # 1234568 — rounded, no decimal
print(f"{n:e}") # 1.234568e+06 — scientific notation
# Alignment and padding
label = "Total"
value = 99.5
print(f"{label:>10}: {value:<10.1f}") # right-align label, left-align value
# Column formatting
for name, score in [("Alice", 92), ("Bob", 78), ("Charlie", 100)]:
print(f"{name:<12}{score:>6}")
# Integer formatting
n = 255
print(f"{n:d}") # 255 — decimal
print(f"{n:b}") # 11111111 — binary
print(f"{n:o}") # 377 — octal
print(f"{n:x}") # ff — hexadecimal
print(f"{n:#x}") # 0xff — hex with prefix
print(f"{n:08b}") # 11111111 — zero-padded to 8 digits

When to use f-strings

Use f-strings by default for any formatting in Python 3.6+. They’re the most readable, fastest at runtime, and support the full format spec.


str.format()

str.format() was the modern approach before f-strings arrived. It supports the same format spec mini-language but embeds values differently:

# Positional arguments
print("Hello, {}!".format("Alice"))
# Indexed arguments
print("{0} and {1}, or {1} and {0}".format("Python", "JavaScript"))
# Named arguments
print("Name: {name}, Score: {score:.1f}".format(name="Alice", score=94.33))
# From a dictionary
data = {"city": "Paris", "country": "France"}
print("{city} is in {country}".format(**data))

When to use str.format()

templates = {
"welcome": "Welcome back, {name}! You have {count} messages.",
"farewell": "Goodbye, {name}. See you soon.",
}
def render(template_key, **kwargs):
return templates[template_key].format(**kwargs)
print(render("welcome", name="Alice", count=3))

You cannot use f-strings for runtime-defined templates because f-strings are evaluated when the source code runs, not when a variable is used. str.format() evaluates at call time.


% Formatting

The oldest method, borrowed from C’s printf. Still found in older codebases and some logging configurations:

name = "Alice"
score = 94.33
print("Name: %s, Score: %.1f" % (name, score))
print("Items in stock: %d" % 42)
print("Hex value: %x" % 255) # ff

Format specifiers: %s (string), %d (integer), %f (float), %x (hex), %.2f (float with 2 decimal places).

When to use % formatting

import logging
logging.basicConfig(level=logging.DEBUG)
username = "alice"
logging.debug("User %s logged in", username) # efficient — no string built if not logged

Using f-strings with logging defeats the performance optimisation:

logging.debug(f"User {username} logged in") # string always built, even if debug is off

Template Strings

string.Template is the fourth option, designed specifically for user-supplied format strings where f-strings or str.format() would be a security risk:

from string import Template
# $ prefix instead of {}
template = Template("Hello, $name! You have $count messages.")
result = template.substitute(name="Alice", count=5)
print(result) # Hello, Alice! You have 5 messages.
# safe_substitute() doesn't fail on missing keys
partial = Template("Dear $name, your order #$id has shipped.")
print(partial.safe_substitute(name="Bob"))
# Dear Bob, your order #$id has shipped.

When to use Template strings

When the format string comes from untrusted input. If a user can supply the format string and you use str.format(), they can access arbitrary attributes of your objects:

# Security problem with format()
class Config:
password = "secret123"
config = Config()
user_template = "{config.__class__.__init__.__globals__}" # malicious input
# This can expose internal data
# Template strings don't have this problem — $ substitution is restricted

For admin tools, email templates loaded from a database, or any situation where users control the template string, use string.Template.


Quick Reference

MethodWhen to use
f-stringDefault for everything in Python 3.6+
str.format()When template is a runtime variable
% operatorIn logging calls; when maintaining old code
string.TemplateWhen users supply the template string

The format spec mini-language (:,.2f, :<10, :>8, :b, etc.) works the same way in f-strings and str.format(). Learn it once and apply it to both.