To format a string in Python, put an f before the quotes and write any variable or expression inside curly braces, like f"Hello {name}". Add a colon and a format spec to control how the value looks, for example f"{price:.2f}" for two decimal places. This guide collects the f-string formatting examples I reach for most: numbers, alignment, dates, debugging and the newer features in Python 3.12 and 3.14.
Before f-strings, I was gluing strings together with + and str(), which got messy quickly. F-strings fixed that. Every example below was run with Python 3.13 (or 3.14 where noted), and the comments at the bottom of each block show the real output.
F-string basics
An f-string (formatted string literal) is a normal string with an f or F prefix. Anything inside {} is evaluated as a Python expression when the line runs, and the result is turned into text.
name = "Mahir"
lessons_done = 7
total = 20
print(f"Hi {name}, you have finished {lessons_done} of {total} lessons.")
print(f"That leaves {total - lessons_done} to go.")
print(f"Your name in capitals is {name.upper()}.")
# Output:
# Hi Mahir, you have finished 7 of 20 lessons.
# That leaves 13 to go.
# Your name in capitals is MAHIR.
You're not limited to variable names. Arithmetic, method calls and function calls all work inside the braces. Keep them short, though. If the expression needs a comment to explain it, calculate it on the line above and put the variable in the f-string.
Formatting numbers with f-strings
The part after the colon is called the format specification. It uses the same "mini-language" as str.format(). These are the number formats I use all the time:
price = 1249.5
ratio = 0.4567
count = 42
views = 1234567
print(f"{price:.2f}") # 2 decimal places
print(f"{price:,.2f}") # thousands separator + 2 decimals
print(f"{ratio:.1%}") # percentage with 1 decimal
print(f"{count:05d}") # pad with zeros to width 5
print(f"{views:,}") # comma groups
print(f"{views:_}") # underscore groups
print(f"{count:b} {count:x} {count:#x}") # binary, hex, hex with prefix
# Output:
# 1249.50
# 1,249.50
# 45.7%
# 00042
# 1,234,567
# 1_234_567
# 101010 2a 0x2a
-
.2fmeans "fixed-point with 2 digits after the decimal point". It rounds, it doesn't just cut off. -
,or_adds a separator every three digits. It goes before the precision:,.2f, not.2f,. -
%multiplies by 100 and adds a percent sign, so store ratios like0.4567, not45.67. -
05dpads an integer with zeros to a width of 5, which is handy for IDs and file names likeimg_00042.png. -
b,oandxshow binary, octal and hex. Add#to include the0b,0oor0xprefix.
Aligning text and building simple tables
When I print results in the terminal, alignment makes them much easier to read. Use < for left, > for right and ^ for centre, followed by a width. You can put a fill character before the alignment symbol.
rows = [("Python", 92), ("HTML", 85), ("JavaScript", 7)]
print(f"{'Subject':<12}|{'Score':>6}")
print("-" * 19)
for subject, score in rows:
print(f"{subject:<12}|{score:>6}")
print(f"[{'hi':^10}]") # centre in 10 characters
print(f"[{'hi':*^10}]") # centre with * as the fill
# Output:
# Subject | Score
# -------------------
# Python | 92
# HTML | 85
# JavaScript | 7
# [ hi ]
# [****hi****]
Strings are left-aligned by default and numbers are right-aligned by default, but I write the alignment out anyway so the intent is obvious. Notice the single quotes inside the double-quoted f-string, like {'Subject':<12}. That works in every Python version that has f-strings.
The = specifier for quick debugging
This is my favourite f-string feature. Put = after an expression and Python prints the expression text, an equals sign and the value. It's much faster than typing labels by hand when you're tracking down a bug.
width = 1200
height = 630
title = "Flexbox vs Grid"
print(f"{width=}, {height=}")
print(f"{width / height = :.2f}")
print(f"{title!r}")
print(f"{title=}")
# Output:
# width=1200, height=630
# width / height = 1.90
# 'Flexbox vs Grid'
# title='Flexbox vs Grid'
A few details worth knowing:
- Spaces around the
=are kept in the output, so{width / height = }prints with spaces. - You can still add a format spec after it:
{width / height = :.2f}. - With
=, Python usesrepr()by default (unless you add a format spec), which is why strings show up with quotes.!rdoes the same thing on its own, and!sforcesstr().
Formatting dates and times
date and datetime objects accept strftime codes directly in the format spec, so you don't need to call .strftime() yourself.
from datetime import date, datetime
exam = date(2026, 11, 15)
start = datetime(2026, 10, 2, 15, 20)
print(f"Exam day: {exam:%d %B %Y}")
print(f"Weekday: {exam:%A}")
print(f"Started at {start:%H:%M} on {start:%Y-%m-%d}")
print(f"ISO format: {exam.isoformat()}")
# Output:
# Exam day: 15 November 2026
# Weekday: Sunday
# Started at 15:20 on 2026-10-02
# ISO format: 2026-11-15
The codes I use most are %d (day), %m (month number), %B (full month name), %Y (four-digit year), %A (weekday), and %H:%M (24-hour time). Month and day names depend on the system locale, so they may not be in English everywhere.
Dynamic widths and precision
The format spec can contain its own braces. This lets you choose the precision or width at runtime, which is useful when the user picks how many decimal places to show.
value = 3.14159265
for digits in (1, 3, 5):
print(f"{value:.{digits}f}")
width = 8
print(f"[{'end':>{width}}]")
# Output:
# 3.1
# 3.142
# 3.14159
# [ end]
Newer f-string features (Python 3.12 and 3.14)
Reusing the same quotes (Python 3.12+)
Since Python 3.12, f-strings are parsed properly by the language grammar (PEP 701). You can reuse the same quote type inside the braces, use backslashes inside expressions and split long expressions over several lines.
# Works on Python 3.12 and newer (PEP 701)
user = {"name": "Nusrat", "city": "Dhaka"}
print(f"{user["name"]} lives in {user["city"]}")
lines = ["first", "second"]
print(f"{'\n'.join(lines)}")
# Output:
# Nusrat lives in Dhaka
# first
# second
On Python 3.11 and older, each of those lines is a SyntaxError. If your code has to run on an older server, stick to switching between single and double quotes.
T-strings: template strings (Python 3.14+)
Python 3.14 added t-strings (PEP 750). They look like f-strings with a t prefix, but instead of returning a finished str, they return a Template object that keeps the static text and the values separate:
# Python 3.14+ only: t-strings return a Template, not a str
from string.templatelib import Template
name = "<b>Rafi</b>"
greeting = t"Hello {name}"
print(type(greeting).__name__)
print(greeting.strings)
print(greeting.interpolations[0].value)
# Output:
# Template
# ('Hello ', '')
# <b>Rafi</b>
That separation means a library can escape the values before joining them, for example to make HTML or SQL safer. For everyday printing and messages, f-strings are still the tool to use.
Common mistakes with f-strings
1. Forgetting the f
Without the prefix, Python doesn't complain. It just prints the braces literally:
name = "Mahir"
print("Hello {name}") # forgot the f
print(f"Hello {name}")
# Output:
# Hello {name}
# Hello Mahir
2. Needing a literal curly brace
To print { or } in an f-string, double it. I hit this whenever I generate CSS or JSON-like text:
selector = "body"
print(f"{selector} {{ margin: 0; }}")
# Output:
# body { margin: 0; }
3. Expecting perfect rounding from floats
Floats are stored in binary, so some decimals can't be stored exactly. 2.675 is really a tiny bit less than 2.675, so it rounds down:
total = 2.675
print(f"{total:.2f}") # floats can't store 2.675 exactly
from decimal import Decimal
print(f"{Decimal('2.675'):.2f}")
# Output:
# 2.67
# 2.68
For money or anything where exact decimals matter, use Decimal, and create it from a string, not a float.
4. Building SQL queries with f-strings
Never put user input into an SQL query with an f-string. That's how SQL injection happens: the input can close your quote and add its own SQL. Use your database library's placeholders instead. Here's a quick test with the built-in sqlite3 module:
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (email TEXT, name TEXT)")
conn.execute("INSERT INTO users VALUES (?, ?)", ("rafi@example.com", "Rafi"))
email = "rafi@example.com' OR '1'='1" # a malicious input
# Unsafe: f"SELECT name FROM users WHERE email = '{email}'" would return every row
rows = conn.execute("SELECT name FROM users WHERE email = ?", (email,)).fetchall()
print(rows) # the placeholder treats the whole input as one plain value
# Output:
# []
With an f-string, that same input turns the WHERE clause into something that's always true, and the query returns Rafi's row even though the email doesn't match. With the ? placeholder, the database treats the whole input as plain data, so nothing matches. The same idea applies in every language. I wrote about the PHP version with PDO prepared statements in another tip.
5. Using f-strings in logging calls
With the logging module, the usual advice is logging.debug("User %s logged in", name) rather than an f-string. Logging only builds the message if that level is actually enabled, while an f-string is always built first.
FAQ
Which Python version do I need for f-strings?
F-strings have been in Python since 3.6. The = debugging specifier needs 3.8 or newer, reusing quotes inside braces needs 3.12, and t-strings need 3.14.
Are f-strings faster than format() and %?
In general, yes: f-strings are usually at least as fast as str.format() and % formatting. I pick them mainly because they're easier to read, since the value sits right where it appears in the text.
How do I write a multi-line f-string?
Use triple quotes with the f prefix: f"""...""". Every line can contain replacement fields. You can also put several f-strings next to each other inside parentheses, and Python joins them into one string.
Can I use a dictionary value inside an f-string?
Yes. On Python 3.12+ you can write f"{user["name"]}". On older versions, use the other quote type: f"{user['name']}".
Conclusion
F-strings put the value right where it appears in the text, and the format spec after the colon controls how it looks: .2f for decimals, , for thousands, % for percentages, < ^ > for alignment, and date codes for dates. Add = when debugging, and you'll rarely need anything else.
For the full list of options, the Python docs have a page on the format specification mini-language. If you want to see what I'm building while learning, have a look at my projects.
Originally published at mfaysal.com.
Top comments (0)