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.