Master string formatting in Python-P3-f-strings

Published On: Wed, 23 Oct 2024 Updated On: Wed, 23 Oct 2024

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.

Code Example
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.

Code Example
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.

Code Example
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:

Code Example
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.

Code Example
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.

Default Alignments of Strings and Numbers in Python

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
Code Example
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.

Numbers and Text Alignments in Python

Let's modify the previous example to display the table demonstrating each row in different alignment.

Code Example
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.

Strings Alignment in Python

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.

Code 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:

String Padding in Python

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.

Code Example
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.

Code 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.

Code 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.

Code Example
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:

Code Example
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:

Code Example
greeting = f"Hello, {name if name else 'Anonymous'}."

Prefer assigning the logic to variables first, and then using the variable inside the f-string:

Code Example
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:

Code Example
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:

Code Example
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:

Code Example
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:

Code Example
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