Master string formatting in Python-P3-f-strings
Introduction
F-String aka "Formatted String Literals" was introduced in Python 3.6. It provides a concise and readable way to interpolate the expressions inside a string using curly braces {}
. This mechanism is designed to be more readable and less error-prone compared to older methods. i.e %-formatting
and str.function()
. As we saw in our previous discussion thread, f-strings can directly embed expressions into strings like keyword arguments in str.format()
method, making it cleaner and easier to read.
In this thread, you will dive into the most potent method to format strings in Python. i.e. Formatted String Literals or F-Strings. So let's get started with the revision first.
Keyword Arguments in str.format()
As you already know we can pass the keyword arguments to the format()
function of the string object that replaces the values appropriately. See the example below.
name="Samuel Jackson"
city = "United States"
"Hi there!, my name is {name}, I am from {city}. Where are you from?".format(name=name, city=city)
# Output
# 'Hi there!, my name is Samuel Jackson, I am from United States. Where are you from?'
You can see how we can embed the keyword arguments into string literals using the format()
method. Let's try to convert this code into f-string syntax.
name="Samuel Jackson"
city = "United States"
f"Hi there!, my name is {name}, I am from {city}. Where are you from?"
# Output
# 'Hi there!, my name is Samuel Jackson, I am from United States. Where are you from?'
Did you see how this is more readable now? If you have seen the difference in both Python versions of the code, you might have observed that we have removed the format method completely and simply prefixed the string with f
in the beginning. Therefore, you now understand that the syntax of the F-string starts with f
or F
. Yes, you can provide use F
as well. Go ahead and try it on your machine and post your comments.
Now that you have got the gist of the formatted string literals, it's time to know its benefits.
Benefits of Using F-strings
Readability
It improves the code readability by reducing the verbosity that exists in the older formatting methodologies. Removing extra code makes sure that the code remains cleaner. Novice developers can easily understand what the statement is doing.
Efficiency
It is more efficient than %-formatting and str.format() methods. F-string is evaluated at runtime, reducing the overhead of the additional function calls.
Flexibility
It can embed various expression types, including variables, arithmetic operations, and function calls. Additionally, it also supports advanced formatting options like alignment, setting width, padding, and controlling precision therefore it is versatile and flexible.
Ease of Use
Because it can embed the expressions inside the string, it reduces human errors by simplifying the coding style and eliminating the placeholders and need for positional arguments promoting the intuitive coding style.
Formatting Data Types
Let's continue our initial example and try experimenting with it with different data types.
name="Samuel Jackson"
city = "United States"
age = 49
height = 5.10
date_of_birth = "16-04-1975"
f"Hi there!, My name is {name}, I am from {city}. I am {age} years old and my height is {height}. I was born on {date_of_birth}."
# Output
# 'Hi there!, My name is Samuel Jackson, I am from United States. I am 49 years old and my height is 5.1. I was born on 16-04-1975.'
The above code contains different data types embedded into the string literal. It successfully processed and converted all data types internally. This is the beauty of the F-Strings. Isn't it easy to read, clean and intuitive? Comment if you differ.
Format Numbers
In real-life scenarios, we need to format numbers in various formats and control their width, precision etc. Below are some examples of formatting numbers in f-strings:
pi = 3.141592653589793238462643383279502884197
age = 40
# Format pi value to 2 precision
print(f"Pi = {pi:.2f}") # Pi = 3.14
# Format pi value to 4 precision
print(f"Pi = {pi:.4f}") # Pi = 3.1416
# Format age value to 2 precision
print(f"Age = {age:.2f}") # Age = 40.00
distance = 1358739598090 # Comma separator in numbers, can provide underscore(_) as well instead of comma
print(f"Distance: {distance:,.0f}" # Distance: 1,358,739,598,090
speed = 299_792_458
print(f"Speed of light is {speed:.2e} m/s") # Speed of light is 3.00e+08 m/s
print(f"Speed of light is {speed:.2E} m/s") # Speed of light is 3.00E+08 m/s
It would help if you did more experiments with real-life usage of this kind. Please mention what additional examples you tried in the comments section. Now let's learn about string alignments.
Aligning Strings
String alignment is also an important part of string formatting. We can align the string in three positions: Left, Center and Right aligned. See the example below to understand it in depth.
from datetime import datetime
from dateutil import parser, relativedelta
data = [{
'first_name': 'Samuel',
'last_name': 'Jackson',
'date_of_birth': '21-Dec-1948',
'height': 6.1,
'occupation': 'Actor'
},{
'first_name': 'Chris',
'last_name': 'Hemsworth',
'date_of_birth': '11-Aug-1983',
'height': 5.11,
'occupation': 'Actor'
},{
'first_name': 'Robert',
'last_name': 'Downey Jr.',
'date_of_birth': '05-Apr-1965',
'height': 5.9,
'occupation': 'Actor'
}]
headers = ('first_name', 'last_name', 'date_of_birth', 'age', 'height', 'occupation')
# Display Table headers
print(
f'{headers[0]:20s}', '|',
f'{headers[1]:20s}', '|',
f'{headers[2]:15s}', '|',
f'{headers[3]:5s}', '|',
f'{headers[4]:15s}', '|',
f'{headers[5]:15s}',
)
print(
f'{"_":_<20s}', '|',
f'{"_":_<20s}', '|',
f'{"_":_<15s}', '|',
f'{"_":_<5s}', '|',
f'{"_":_<15s}', '|',
f'{"_":_<15s}',
)
# Display Table Data
for item in data:
delta = datetime.today() - parser.parse(item[headers[2]])
age = delta.days//365
print(
f'{item[headers[0]]:20s}', '|',
f'{item[headers[1]]:20s}', '|',
f'{item[headers[2]]:15s}', '|',
f'{age:5d}', '|',
f'{item[headers[4]]:15f}', '|',
f'{item[headers[5]]:15s}',
)
In the above example, we tried to use different data types formatting. You must have observed that float and integer formatting are right aligned by default. However, we are free to change them if we want. This is your exercise and all who have tried, please mention the comments.
Let's try to align each table row to a different alignment so that visibly you can differentiate them. Before that see the below symbols for your reference.
Symbol | Meaning |
< | Left-Aligned/Right-side Padding |
^ | Center Aligned/Both-sided Padding |
> | Right Aligned/Left-side Padding |
numbers = [5000, 123456, 98765428]
print("Left-Aligned Number:")
for num in numbers:
print('|', f'{num:<15}', '|')
print("\nCenter-Aligned Number:")
for num in numbers:
print('|', f'{num:^15}', '|')
print("\nRight-Aligned Number -> Default for Numbers:")
for num in numbers:
print('|', f'{num:15}', '|')
texts = ["Left", "Center", "Right"]
print("\nLeft-Aligned Text -> Default For Strings:")
print('|', f'{texts[0]:15}', '|')
print("\nCenter-Aligned Text:")
print('|', f'{texts[1]:^15}', '|')
print("\nRight-Aligned Text:")
print('|', f'{texts[2]:>15}', '|')
Observer the below output.
Let's modify the previous example to display the table demonstrating each row in different alignment.
from datetime import datetime
from dateutil import parser
data = [{
'first_name': 'Samuel',
'last_name': 'Jackson',
'date_of_birth': '21-Dec-1948',
'height': 6.1,
'occupation': 'Actor'
},{
'first_name': 'Chris',
'last_name': 'Hemsworth',
'date_of_birth': '11-Aug-1983',
'height': 5.11,
'occupation': 'Actor'
},{
'first_name': 'Robert',
'last_name': 'Downey Jr.',
'date_of_birth': '05-Apr-1965',
'height': 5.9,
'occupation': 'Actor'
}]
headers = ('first_name', 'last_name', 'date_of_birth', 'age', 'height', 'occupation')
print()
print('$')
# Display Table headers
print(
f'{headers[0]:20s}', '|',
f'{headers[1]:20s}', '|',
f'{headers[2]:15s}', '|',
f'{headers[3]:5s}', '|',
f'{headers[4]:15s}', '|',
f'{headers[5]:15s}',
)
# Left-side Padding example using underscore '_' character
print(
f'{"_":_<20s}', '|',
f'{"_":_<20s}', '|',
f'{"_":_<15s}', '|',
f'{"_":_<5s}', '|',
f'{"_":_<15s}', '|',
f'{"_":_<15s}',
)
alignment = ('<', '^', '>')
align_i = 0
# Display Table Data
for item in data:
delta = datetime.today() - parser.parse(item[headers[2]])
age = delta.days//365
print(
f'{item[headers[0]]:{alignment[align_i]}20s}', '|',
f'{item[headers[1]]:{alignment[align_i]}20s}', '|',
f'{item[headers[2]]:{alignment[align_i]}15s}', '|',
f'{age:{alignment[align_i]}5d}', '|',
f'{item[headers[4]]:{alignment[align_i]}15f}', '|',
f'{item[headers[5]]:{alignment[align_i]}15s}',
)
align_i += 1 # Pick next alignment operator
In this example, we tried to use expressions inside expressions. We tried to use each alignment operator in each row that generated below output.
Padding In Strings
These operators are also used for padding the strings to fill up the space. You might have noticed the code in our previous example where I printed the table headers. Let's revise it here with a new example.
num = ["right-side-padding", 'both-side-padding', 'left-side-padding']
print("Right-side Padded Text -> Default:")
print('|', f'{num[0]:*<50}', '|')
print("\nBoth-Side Padded Text:")
print('|', f'{num[1]:*^50}', '|')
print("\nLeft-side padded Text:")
print('|', f'{num[2]:*>50}', '|')
The meaning of the padding operators is exactly the opposite compared to text alignment. See the below output:
Formatting Date and Time
The beauty of the f-string is that it makes the date and time formatting very convenient. Let's see the examples.
from datetime import datetime
# Get the current date and time
now = datetime.now()
# Format Date and Time in DD-MM-YYYY HH:MM:SS
formatted_now_full = f"{now: %d-%m-%Y %H:%M:%S}"
Nesting of f-Strings
f-strings can also be nested, which allows more dynamic formatting. See the below example.
value = 10
nested_fstring = f"The result is: {f'{value * 2:.2f}'}"
print(nested_fstring) # Output: The result is: 20.00
Using f-Strings with Custom Objects and __format__
Consider you are creating an application and it has several class objects. Now, what would happen if you try to print the object itself? See the below example.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
person = Person("Alice", 30)
print(f"{person}") # Output: Alice, Age: 30
Now, this is very annoying when you want to see what is there in the object but you get something like this. So, what you can do here?
Don't worry!! See the same example with little modification below.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __format__(self, format_spec):
return f"My name is {self.name} and I am {self.age} year(s) old."
def __str__(self):
return f"My name is {self.name}."
person = Person("Alice", 30)
print(f"(Format Method) => {person}") # Output: Alice, Age: 30
print(f"(str function) => {str(person)}") # Output: Alice, Age: 30
Above, you can observe that we used two methods in class definition. These methods are factory methods which we can override in any class to change the behavour of our own. See the differrence in output of this program and previous one.
You can make out that when we simply printed the object ```person```, ```__format__()``` method gets called and when we convert the object to string representation using str(), ```__str__()``` method gets called.
You can specify any appropriate format specification using function parameters and use them in method definition to represent objects as per your requirements.
Common Pitfalls and How to Avoid Them
Escaping Braces
One common issue with f-strings is escaping curly braces, which are used for both string formatting and as literals. To include literal {
or }
in your f-strings, you must double them:
literal_braces = f"Curly braces: {{ and }}"
print(literal_braces) # Output: Curly braces: { and }
Expressions Only Inside Braces
It’s important to remember that f-strings can only contain expressions within the braces {}
. Using assignment or control flow statements directly inside the curly braces will result in a syntax error.
Unique Tip and Best Practices for Large-Scale Applications
When working on large codebases, f-strings can significantly improve readability. However, overuse or complex inline expressions can hurt maintainability. Keep your expressions simple within f-strings, and avoid embedding too much logic directly.
For example, instead of writing:
greeting = f"Hello, {name if name else 'Anonymous'}."
Prefer assigning the logic to variables first, and then using the variable inside the f-string:
name_to_use = name if name else "Anonymous"
greeting = f"Hello, {name_to_use}."
This approach makes your code easier to debug and maintain.
Integration with Other Libraries
Using f-Strings with Logging
Python’s logging library can benefit from f-strings for more readable log messages:
import logging
logging.basicConfig(level=logging.INFO)
name = "Alice"
logging.info(f"User {name} has logged in.")
However, note that for performance reasons, it’s still recommended to use lazy string interpolation with logging (%s
formatting), especially for log statements that may not always be executed:
logging.info("User %s has logged in.", name)
f-Strings in Unit Testing and Debugging
When debugging or writing unit tests, f-strings can be incredibly useful for generating detailed error messages:
def add(a, b):
return a + b
a, b = 5, 10
expected = 15
assert add(a, b) == expected, f"Expected {expected}, but got {add(a, b)}"
This provides a clear and concise error message if the assertion fails.
Performance Benchmarks of f-Strings
f-strings are not only more readable but also faster than older string formatting methods. A simple performance benchmark:
import timeit
name = "Alice"
age = 30
time_percentage = timeit.timeit("'Hello, %s. You are %d years old.' % (name, age)", globals=globals())
time_format = timeit.timeit("'Hello, {}. You are {} years old.'.format(name, age)", globals=globals())
time_fstring = timeit.timeit("f'Hello, {name}. You are {age} years old.'", globals=globals())
print(f"% formatting: {time_percentage}")
print(f".format method: {time_format}")
print(f"f-string: {time_fstring}")
In most cases, f-strings outperform both %
formatting and .format()
methods due to their efficiency.
Conclusion
f-strings are a powerful and efficient way to handle string formatting in Python. They not only improve readability but also provide better performance than older methods. Whether you are working with simple variable substitution or complex object formatting, f-strings offer a concise and flexible solution for all your string interpolation needs.
With f-strings, you can write cleaner, more maintainable code that is easier to understand and less prone to errors. From basic usage to advanced techniques like nested f-strings and custom object formatting, this guide covers everything you need to become proficient with f-strings in Python.
Reader Comments
Add a Comment
Recent Posts
- Advantages of Using GNU Sed | Enhance Your Text Processing
- An Introduction to Mojo Programming Language
- Guiding the Path to Engagement | Practical Prompt Engineering
- Mastering the Art of Writing Perfect ChatGPT Prompts | A Comprehensive Guide With Examples
- What is Sedition Law in India? | Origins, History, and Implications
- Everything You Need to Know About Encapsulation in Python: A Deep Dive
- Is Python and Django Enough to Conquer the Web Development Realm?
- How to Fix the "No Python Found" Error in Pipenv on Windows: A Comprehensive Guide
- Master string formatting in Python-P1-Legacy Formatting
- Master string formatting in Python-P2-str.format()