DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

f-strings and String Formatting in Python

You've already seen python f-strings scattered throughout earlier articles in this series — they're simply the cleanest way to combine text and values in Python. This article goes deeper: the full formatting mini-language behind that {value:...} syntax, what came before f-strings, and a few patterns worth knowing once you're comfortable with the basics.

What Are f-strings?

An f-string is a string literal prefixed with f (or F) that lets you embed expressions directly inside curly braces {}. Introduced in Python 3.6 through PEP 498, they've become the modern standard for python string formatting.

name = "Alex"
age = 30

print(f"{name} is {age} years old")
# Alex is 30 years old

Whatever's inside the braces gets evaluated and inserted directly into the string at that position — no separate formatting call, no placeholder tracking.

Why f-strings Over Older Methods

Python has actually had three distinct eras of string formatting, and it's worth knowing all three, since you'll still encounter the older ones in existing code.

The three eras
name = "Alex"

# Era 1: the % operator (oldest)
print("%s is here" % name)

# Era 2: the .format() method
print("{} is here".format(name))

# Era 3: f-strings (current standard)
print(f"{name} is here")
Why f-strings win

f-strings are generally preferred now for a few concrete reasons:

  • Readability — the value sits directly where it's used, instead of being tracked separately at the end of the string (as with %) or referenced by position/name elsewhere (as with .format()).

  • Performance — f-strings are evaluated at compile time in a way that's typically faster than the alternatives, since Python doesn't need to parse a separate format string and match up arguments at runtime.

  • Conciseness — no repeated placeholders, no .format() call to close out, no separate %-style conversion characters to remember.

Where the older styles still show up

You'll still run into % formatting and .format() in legacy codebases, certain logging libraries (Python's built-in logging module still commonly uses %-style formatting internally for performance reasons), and in code written before Python 3.6. Recognizing them is useful even if you rarely write them yourself going forward.

The Format Specification Mini-Language

This is where f-strings go from "convenient" to genuinely powerful. Python string formatting supports a full mini-language, invoked with a colon inside the braces: {value:format_spec}.

Number formatting: decimal precision
price = 49.5
print(f"")   # $49.50 — rounds to 2 decimal places
Thousands separators
population = 1000000
print(f"{population:,}")     # 1,000,000 — comma separator
print(f"{population:_}")     # 1_000_000 — underscore separator
Padding and zero-fill
number = 7
print(f"{number:05}")   # 00007 — pads with zeros to a total width of 5

This is useful for things like consistently formatted IDs, timestamps, or file numbering, where you want a fixed visual width regardless of the actual value's length.

Alignment and width

You can control alignment within a fixed-width field using < (left), > (right), and ^ (center), optionally paired with a custom fill character:

print(f"{'left':<10}|")     # left      |  — left-aligned, padded to 10 chars
print(f"{'right':>10}|")    #      right|  — right-aligned, padded to 10 chars
print(f"{'mid':^10}|")      #    mid    |  — centered within 10 chars
print(f"{'pad':*^10}|")     # ***pad****|  — centered, with * as the fill character

This combination — width plus alignment plus a fill character — is exactly what you'd reach for when building simple aligned table output in the console.

Date and time formatting

f-strings can format datetime objects directly, using the same strftime codes you'd pass to .strftime():

from datetime import datetime

now = datetime(2026, 7, 9)
print(f"{now:%Y-%m-%d}")     # 2026-07-09
print(f"{now:%B %d, %Y}")    # July 09, 2026

No separate .strftime() call needed — the format spec after the colon handles it inline.

Beyond Simple Variables

Embedding expressions and function calls

The curly braces in an f-string aren't limited to plain variable names — any valid Python expression works, including function calls and arithmetic:

price = 49.5
tax_rate = 0.08

print(f"Total with tax: {price * (1 + tax_rate):.2f}")

def get_greeting():
    return "Hello"

print(f"{get_greeting()}, world!")
Conditional logic inline

You can even embed a conditional expression directly:

age = 15
print(f"You are {'an adult' if age >= 18 else 'a minor'}")

Used sparingly, this is a compact way to handle simple either/or text — but it's worth resisting the temptation to cram anything more complex than a one-line conditional into an f-string. If the logic needs more than that, compute it on a separate line first, then reference the resulting variable inside the f-string.

Accessing dictionary keys, object attributes, and list items

f-strings handle these all naturally, without any special syntax beyond what you'd normally write:

user = {"name": "Alex", "age": 30}
print(f"{user['name']} is {user['age']} years old")

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 4)
print(f"Point at ({p.x}, {p.y})")

items = ["apple", "banana", "cherry"]
print(f"First item: {items[0]}")
The debug specifier (=)

Python 3.8 added a genuinely handy shortcut for debugging: appending = right before the closing brace prints both the expression's text and its value, without you having to type the variable name twice.

name = "Alex"
age = 30

print(f"{name=}")   # name='Alex'
print(f"{age=}")    # age=30
print(f"{age * 2=}")   # age * 2=60

This is a small but genuinely useful trick for quick debugging — dropping a print with {variable=} gives you both the label and the value in one line, instead of writing print(f"name: {name}") manually.

Conversion flags

You can also control exactly how a value is converted to a string using conversion flags — !s (str), !r (repr), and !a (ascii):

text = "café"

print(f"{text!s}")   # café — regular string conversion (the default)
print(f"{text!r}")   # 'café' — repr, includes quotes, useful for debugging
print(f"{text!a}")   # 'caf\xe9' — ascii-only representation, escaping non-ASCII characters

!r in particular is worth knowing — it's a common choice when logging or debugging, since it makes it visually obvious that a value is a string (surrounded by quotes) rather than some other type that happens to print similarly.

Practical Patterns and Best Practices

Real-world use cases

f-strings show up constantly in:

  • Log messages — combining a timestamp, log level, and message into one readable line.

  • Formatted reports and invoices — aligning columns, formatting currency, and applying consistent decimal precision.

  • Aligned table output — using width and alignment specifiers to keep console output readable across rows.

items = [("Apple", 1.50), ("Bread", 3.25), ("Milk", 2.10)]

for name, price in items:
    print(f"{name:<10}")
Nesting format specs dynamically

You can even make the width or precision itself a variable, by nesting another set of braces inside the format spec:

value = 3.14159
width = 10
precision = 2

print(f"{value:{width}.{precision}f}")
#     '      3.14' — width and precision both pulled from variables

This is useful when formatting needs to adapt based on some other calculated value — for example, aligning columns to the width of the longest entry in a dataset, determined at runtime rather than hardcoded.

Security note: never build f-strings from untrusted input

This is worth taking seriously: f-strings evaluate real Python expressions. If you ever find yourself constructing an f-string's template dynamically from user-supplied text — rather than just plugging user data into a fixed, hardcoded f-string you wrote yourself — you're opening the door to arbitrary code execution. Never do this:

# Dangerous — never do this
user_supplied_template = get_untrusted_input()
result = eval(f'f"{user_supplied_template}"')

The f-strings you write directly in your source code are completely safe — the risk only appears if you try to build the format string itself from untrusted input, rather than just inserting untrusted values into a template you control.

When to reach for string.Template instead

If you genuinely need to accept a format string from an untrusted source — a configuration file, user input, an external template — Python's built-in string.Template class is the safer tool for that specific job. It uses a much more restricted placeholder syntax ($name rather than arbitrary expressions), which means it can't be exploited to execute arbitrary code the way a dynamically constructed f-string could:

from string import Template

template = Template("Hello, $name!")
print(template.substitute(name="Alex"))   # Hello, Alex!

For everyday code where you're writing the format string yourself, f-strings remain the right default — string.Template is specifically for the narrower case of handling format strings that come from somewhere you don't fully trust.

PREVIOUSNEXT LESSON