Python's Format Mini-Language for Tidy Strings

Python's Format Mini-Language for Tidy Strings

by Leodanis Pozo Ramos Jan 31, 2024 intermediate python

When you’re doing string interpolation in your Python code, you often need to format the interpolated values to meet some formatting requirements. To do this, Python provides what is known as the format mini-language, which defines the syntax of a format specifier.

Perhaps you’re comfortable working with strings, but you want to take even more control of them. With proficiency in the format mini-language, you’ll be able to use format specifiers to do things like formatting numbers as currency values, using scientific notation, expressing a value as a percentage, and so much more.

In this tutorial, you’ll:

  • Learn the format mini-language syntax
  • Align and fill textual output in your code
  • Convert between data types in your outputs
  • Provide format fields dynamically
  • Format numeric values in different ways

To get the most out of this tutorial, you should be familiar with Python’s string interpolation tools, such as the str.format() method and f-strings.

Using String Interpolation and Replacement Fields

When you’re working with strings, it’s common that you need to embed or insert values and objects into your strings so that you can build new strings dynamically. This task is commonly known as string interpolation.

In Python, you’ll find three popular tools that allow you to perform string interpolation:

  1. The modulo operator (%)
  2. The str.format() method
  3. F-string literals

The modulo operator is sort of an old-fashioned tool. It was the first string interpolation tool in Python. Unfortunately, it doesn’t provide many string formatting features. So, in this tutorial, you’ll focus on the str.format() method and f-strings.

To dynamically interpolate a value into a string, you need something called replacement fields. In both str.format() and f-strings, curly braces ({}) delimit replacement fields. Inside these braces, you can put a variable, expression, or any object. When you run the code, Python replaces the field with the actual value.

Anything not contained in braces is considered literal text, and Python copies it unchanged to the output.

In the following sections, you’ll learn how replacement fields work in str.format() and f-strings.

The str.format() Method

You can use str.format() to interpolate values into your strings. This method operates on a string object where you insert replacement fields as needed. Then Python interpolates the arguments to .format() into the string to build the final string dynamically:

>>> "Hello, {}!".format("Pythonista")
'Hello, Pythonista!'

In this example, you have a string containing a replacement field. Then, you call .format() with a single argument. When you run this code, the method inserts its argument into the replacement field and builds a final string.

You can use .format() in several ways. In the example above, the replacement field is empty. However, you can use zero-based indices to define a specific insertion order. You can also use named fields:

>>> "Hello, {0}! Good {1}!".format("Pythonista", "morning")
'Hello, Pythonista! Good morning!'

>>> "Hello, {name}! Good {moment}!".format(
...    name="Pythonista", moment="morning"
... )
'Hello, Pythonista! Good morning!'

In the first example, you use integer indices to define the order in which you want to insert each argument into the replacement fields. In the second example, you use explicit argument names to insert the values into the final string.

The Python documentation uses the following Backus–Naur form (BNF) notation to define the syntax of a replacement field for the .format() method:

BNF Grammar
replacement_field ::=  "{"
                            ["!" conversion]
                            [":" format_spec]

From this BNF rule, you can conclude that the field name is optional. After that, you can use an exclamation mark (!) to provide a quick conversion field. This field can take one of the following forms:

  • !s calls str() on the argument.
  • !r calls repr() on the argument.
  • !a calls ascii() on the argument.

As you can see, the conversion field allows you to use different string representations for the value that you want to interpolate into your string.

As an example of how the conversion field works, say that you have the following Person class:

class Person:
    def __init__(self, name, age): = name
        self.age = age

    def __str__(self):
        return f"I'm {}, and I'm {self.age} years old."

    def __repr__(self):
        return f"{type(self).__name__}(name='{}', age={self.age})"

In this class, you have two instance attributes, .name and .age. Then, you have .__str__() and .__repr__() special methods to provide user-friendly and developer-friendly string representations for your class, respectively.

To learn how the conversion field works, go ahead and run the following code:

>>> from person import Person

>>> jane = Person("Jane Doe", 25)

>>> "Hi! {!s}".format(jane)
"Hi! I'm Jane Doe, and I'm 25 years old."

>>> "An instance: {!r}".format(jane)
"An instance: Person(name='Jane Doe', age=25)"

In these examples, you use the !s and !r conversion fields with different purposes. In the first example, you get a user-friendly message. In the second example, you get a developer-friendly message.

The final part of the replacement field syntax is also optional. It’s a string that must start with a colon (:) and can continue with a format specifier. You’ll learn about format specifiers in a moment. For now, you’ll continue with f-strings and learn how they implement replacement fields.


You’ll find that the formatted string literal, or f-string, is quite a popular tool for string interpolation and formatting. F-string literals start with f or F. To interpolate a value into an f-string, you need replacement fields. Like with str.format(), the replacement fields are delimited by a pair of curly brackets ({}).

The f-string syntax is pretty concise, which is a reason why f-strings are so popular. You don’t need to call a method or use an operator. You only need to embed the target object into the replacement field:

>>> name = "Pythonista"
>>> moment = "morning"

>>> f"Hello, {name}! Good {moment}!"
'Hello, Pythonista! Good morning!'

In this example, you define two variables to provide the values that you want to interpolate into your string. Then, you use these variables directly in the replacement fields. That’s cool, isn’t it?

The BNF rule that defines the syntax of a replacement field in an f-string is pretty similar to the one that defines the syntax of a replacement field in the str.format() method:

BNF Grammar
replacement_field ::=  "{"
                             ["!" conversion]
                             [":" format_spec]

The f_expression part refers to the value or expression that you want to insert into your string. Note that this field is required. Then, you have an optional equal sign (=), which enables the self-documented expressions.

Next, you have the familiar conversion field, which works the same as in the str.format() method:

>>> from person import Person

>>> jane = Person("Jane Doe", 25)

>>> f"Hi! {jane!s}"
"Hi! I'm Jane Doe, and I'm 25 years old."

>>> f"An instance: {jane!r}"
"An instance: Person(name='Jane Doe', age=25)"

Again, with !s, you get a user-friendly string representation, while with !r, you get a developer-friendly representation.

Now that you know how replacement fields work in str.format() and f-strings, you’ll learn how to format the embedded values in your strings. This process is commonly known as string formatting and takes advantage of format specifiers and the format specifier mini-language.

Understanding the Python Format Mini-Language

You can use format specifiers within replacement fields to format the object that you want to interpolate into your strings. In general, an empty format specifier produces the same result as if you had called str() with the value as an argument. Meanwhile, a non-empty format specification typically modifies the result.

The syntax to define working format specifiers is a bit involved. Because of that, the syntax is considered a whole mini-language. Here’s the BNF notation that defines it:

BNF Grammar
format_spec     ::=  [[fill]align][sign]["z"]["#"]["0"][width]
                     [grouping_option]["." precision][type]
fill            ::=  <any character>
align           ::=  "<" | ">" | "=" | "^"
sign            ::=  "+" | "-" | " "
width           ::=  digit+
grouping_option ::=  "_" | ","
precision       ::=  digit+
type            ::=  "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" |
                     "G" | "n" | "o" | "s" | "x" | "X" | "%"

The first BNF rule defines the general syntax, while the following rules define every independent field. For example, the fill field accepts any character. The align field can be one of the following characters: <, >, =, or ^. The rest of the rules have other possible values as well.

Reading and understanding the BNF notation can be challenging. So, in the following sections, you’ll learn how all the above fields work in practice and how you can combine them to create a suitable format specifier. To kick things off, you’ll start with the fields closely related to the output alignment.

Aligning and Filling the Output

When you start writing your own format specifiers, you’ll find that the fill, width, and align fields are closely related. With them, you can align the interpolated value in the final string.

The width field provides the working space, which consists of a given number of allowed characters. The align field allows you to position the interpolated value within the working space. Finally, the fill field lets you fill out the leftover space with the character of your preference.

To provide a value to fill, you can use any character. Python will use this character to fill the space around the interpolated value. Similarly, to provide a value to width, you can use an integer number.

The align field requires a bit of explanation, which you’ll find in the following table:

Value Description
< Aligns the interpolated value to the left within the available space. It’s the default alignment for most objects.
> Aligns the interpolated value to the right within the available space. It’s the default alignment for numbers.
^ Aligns the interpolated value in the center of the available space.
= Adds padding after the sign but before the digits in numeric values. It’s the default for numbers when 0 immediately precedes the field width.

These alignment values allow you to control how a given value is displayed in the resulting string. To illustrate how the fill, width, and align fields work in practice, consider the following examples:

>>> text = "Hello!"

>>> # width=30
>>> f"{text:30}"
'Hello!                        '

>>> # align="<" and width=30
>>> f"{text:<30}"
'Hello!                        '

>>> # align="^" and width=30
>>> f"{text:^30}"
'            Hello!            '

>>> # align=">" and width=30
>>> f"{text:>30}"
'                        Hello!'

>>> # fill="=", align="^" and width=30
>>> f"{text:=^30}"

With the width field, you can define the number of total characters that your replacement field will have. In this example, you use a width of 30 characters in total. The following three examples use different align values to align the word "Hello!" to the left, center, and right within the space defined by the width field.

In the final example, you use an equal sign (=) to fill the space on both sides of the interpolated value. How does that look?

Note that you need to follow the specified order of fields for your format specifier to work. As the syntax defines, you need to have fill, align, and then width, in that order. If you violate the order, then you get an error:

>>> f"{text:30>}"
Traceback (most recent call last):
ValueError: Unknown format code '>' for object of type 'str'

In this case, you place the width field before the align field. This ordering causes Python to raise a ValueError exception.

Converting Between Type Representations

A common requirement when you’re formatting your strings with format specifiers is to display an object using a different type representation. To represent string values, you can use the s representation type. Because this is the default, you can also omit it:

>>> name = "Pythonista"

>>> f"Hello, {name:s}!"
'Hello, Pythonista!'

>>> f"Hello, {name:}!"
'Hello, Pythonista!'

In this example, you use the s representation type to display a string. Note that if you provide the s representation type, then your format specifier won’t accept other types:

>>> f"{42:s}"
Traceback (most recent call last):
ValueError: Unknown format code 's' for object of type 'int'

When you set the s representation type in a format specifier, then you’ll get a ValueError when you try to pass an object of a different type, like the integer value in the above example.

When it comes to numeric values, you may want to display an integer value as a hexadecimal value or maybe as a binary value. To achieve this, the format mini-language offers you several type modifiers. The available type modifiers for representing integer values are the following, with decimal (d) as the default:

Representation Type Type Description
b Binary Converts the number to base 2
c Character Converts the number to the corresponding Unicode character
d Decimal Integer Converts the number to base 10
o Octal Converts the number to base 8
x or X Hexadecimal Converts the number to base 16, using lowercase or uppercase letters for the digits above 9
n Number Works the same as d, except that it uses the current locale setting to insert the appropriate thousand separator characters
None Decimal Integer Works the same as d

You can convert integer values using different representation types with all these type modifiers. Note that you shouldn’t confuse None in the table above with Python’s None object. In this context, None means that you can omit the type specifier.

Here’s a small example:

>>> number = 42

>>> f"int: {number:d},  hex: {number:x},  oct: {number:o},  bin: {number:b}"
'int: 42,  hex: 2a,  oct: 52,  bin: 101010'

This f-string uses different type modifiers to convert an integer number to other types. Note that the type modifiers do the hard work for you, converting the value from one representation to another.

The type modifiers that you saw above aren’t the only ones available in Python’s format mini-language. It also provides modifiers for decimal types:

Representation Type Description
e or E Scientific notation with the separator character in lowercase or uppercase, respectively
f or F Fixed-point notation with nan and inf in lowercase or in uppercase, respectively
g or G General format where small numbers are represented in fixed-point notation and larger numbers in scientific notation
n General format (same as g), except that it uses a locale-aware character as a thousand separator

You can use the above type modifiers to express floating-point numbers using different representations. Here are a few toy examples of how these modifiers work in practice:

>>> large = 1234567890
>>> f"{large:e}"
>>> f"{large:E}"

>>> number = 42.42
>>> f"{number:f}"
>>> inf = float("inf")
>>> f"{inf:f}"
>>> f"{inf:F}"

>>> f"{large:g}"
>>> f"{large:G}"
>>> f"{number:g}"

In the first set of examples, you use e and E to represent a large number using scientific notation.

Next, you use the f and F type modifiers. The difference between these two is that the inf and nan values will use lowercase letters with f and uppercase letters with F.

Finally, g and G represent larger numbers in scientific notation and smaller numbers in fixed-point notation. The difference between g and G is that when you use them with large numbers, the scientific notation separator will be in lowercase or uppercase, respectively.

Formatting Numeric Values to Improve Presentation

The format specifier mini-language has several options to format numeric values. You can use these options to tweak how you display numbers in your program’s output. You’ll have options to control the decimal precision, add thousand separators, express the numbers as percentages, and add leading signs to your numeric values.

In the following sections, you’ll learn about all these options and how to use them as part of your format specifiers.

Setting a Decimal Precision

The precision field in the BNF grammar of a format specifier accepts an integer value indicating how many digits should follow the decimal point for the f and F presentation types. This field is pretty useful because it allows you to display floating-point values in a uniform way.

In the following example, you’ll represent the value of pi using different precisions. To set the precision, you need to explicitly use a dot followed by the precision value and then the f or F type:

>>> from math import pi

>>> pi

>>> f"{pi:.4f}"

>>> f"{pi:.8f}"

In this example, you use a precision of 4 and then 8 to display the value of pi. Note that the resulting number is automatically rounded when displayed, which is another effect of using the precision field in your format specifiers.

Using Thousand Separators

The format specifier syntax also provides a grouping_option field to group numeric values conveniently and improve readability. This field allows you to format numeric values using a specific thousand separator. The field can take one of two values:

Separator Description
, Allows you to use a comma as a thousand separator
_ Allows you to use an underscore as a thousand separator

These two values work for the fixed-point presentation types and the d (decimal) integer presentation type.

To understand how the grouping_option field works, consider the following examples:

>>> number = 1234567890

>>> f"{number:,}"

>>> f"{number:_}"

>>> f"{number:,.2f}"

>>> f"{number:_.2f}"

In the first two examples, you use a comma and an underscore as thousand separators in integer values. Note that you’ll get the same result using the d integer presentation type.

In the final two examples, you use a comma and an underscore to group thousands. Note that in this case, you’re converting the original integer number and expressing it as a fixed-point number with a precision of 2.

Adding Signs

The sign field in the format specifier syntax will allow you to tweak how you represent numbers that include a sign. This field can take one of the following values:

Value Meaning
+ Indicates that a sign should be used for both positive and negative numbers
- Indicates that a sign should be used only for negative numbers, which is the default behavior
space Indicates that a leading space should be used on positive numbers and a minus sign on negative numbers

With these values, you can fine-tune how you present signs when displaying numeric values. Here’s a short example of how the sign field operates:

>>> positive = 42
>>> negative = -42

>>> f"{positive:+}"
>>> f"{negative:+}"

>>> f"{positive:-}"
>>> f"{negative:-}"

>>> f"{positive: }"
' 42'
>>> f"{negative: }"

In the first two examples, you use the plus sign (+) to always show a sign in front of your numeric values. In the next two examples, the minus sign (-) allows you to display positive numbers without a sign and negative numbers with a negative sign.

Finally, you use a space to display positive numbers with a leading space and negative numbers with the corresponding negative sign.

Providing Formatting Fields Dynamically

Up to this point, you’ve learned the basics of Python’s string formatting mini-language. In all the examples you’ve seen so far, you’ve hard-coded the format specifiers using a string. However, sometimes you’ll need to build the format specifier dynamically.

To do this, you can embed variables and expressions in the format specifier using curly brackets. Consider the following example:

>>> total = 123456.99

>>> # Formatting values
>>> width = 30
>>> align = ">"
>>> fill = "."
>>> precision = 2
>>> sep = ","

>>> f"Total{total:{fill}{align}{width}{sep}.{precision}f}"

In this example, you build a format specifier using variables within curly brackets. To improve readability, you use mostly the same names as those in the format specifier syntax, except for sep, which represents the grouping option.

The possibility of dynamically creating format specifiers is a powerful tool that allows you to add flexibility to your string formatting code.

Formatting Strings: Practical Examples

Now that you’ve learned the basic syntax of format specifiers and seen how to generate them dynamically, it’s time to dive into some practical examples of when you’d like to use format specifiers in your strings.

To kick things off, you’ll start with a pretty common use case of string formatting: representing currency values. Then, you’ll explore other common use cases of format specifiers.

Representing Currency Values

Formatting numbers as currency values can be a common requirement in your code. You can craft your format specifiers to meet this need. For example, say that you want to create a report listing your inventory of products with their prices.

To do this, you can use the following code:

>>> inventory = [
...     {"product": "Apple", "price": 5.70},
...     {"product": "Orange", "price": 4.50},
...     {"product": "Banana", "price": 6.00},
...     {"product": "Mango", "price": 8.60},
...     {"product": "Pepper", "price": 4.20},
...     {"product": "Carrot", "price": 3.57},
... ]

>>> for item in inventory:
...     product = item["product"]
...     price = item["price"]
...     print(f"{product:.<30}${price:.2f}")

In this example, you first define a dictionary holding an inventory of fruits and vegetables. In the loop, you iterate over the items in the inventory, get the product’s name and price, and finally print a formatted string containing the required information.

In the format specifier, you use a dot as the filling character. Then, you align the product’s name to the left. Finally, you display the product’s price using a precision of 2. The dollar sign makes the value look like a currency value.

Formatting Dates

Another common use case of string formatting is when you need to format date and time values. If you’re using the datetime module, then it’s great to know that the date formatting codes work nicely with f-strings or the .format() method. So, you can use the date and time format codes to create format specifiers when you’re dealing with dates:

>>> from datetime import datetime

>>> now =
>>> now
datetime.datetime(2024, 1, 31, 14, 6, 53, 251170)

>>> >>> f"Today is {now:%a %b %d, %Y} and it's {now:%H:%M} hours"
"Today is Wed Jan 31, 2024 and it's 14:06 hours"

In this example, you use now() from datetime to get the current date and time. Of course, when you run this code, you’ll get a different date and time. Then, you create an f-string that uses the date and time format codes as format specifiers. These codes allow you to create nicely formatted dates.

Expressing Percentages

Another cool formatting that you can obtain using a format specifier is the percentage format. The % modifier allows you to express a number as a percentage. Under the hood, this modifier multiplies the number by 100, displays it in fixed-point notation, and adds a percent sign at the end.

For example, say that you want to compute the winning percentage of a basketball team. In this case, you can define a precision of 2 and then include the % modifier like in the following code:

>>> wins = 25
>>> games = 35

>>> f"Team's winning percentage: {wins / games:.2%}"
"Team's winning percentage: 71.43%"

Note how the % modifier allows you to express the result of dividing the number of wins by the total number of games as a winning percentage. Note that the precision isn’t required. You can use the % modifier by itself, in which case you’ll get as many decimal digits as Python returns.

It’s important not to confuse this use of the percentage symbol (%) with the modulo operator. In the context of a format specifier, the symbol has a different meaning.

Exploring Other Options in the Mini-Language

At this point, you’ve learned the string format mini-language’s major features. But there’s still more to discover! In this section, you’ll learn some more options that might come in handy when you’re creating your own format specifiers. In practice, you’ll learn about two additional modifiers:

  • z coerces negative zeros.
  • # adds type representation prefixes.

In Python 3.11 the format specifier syntax was extended to include z. The z modifier coerces negative zero floating-point values to positive zero after rounding to the format precision. This option works only for floating-point presentation types.

Yes, Python has negative zero values:

>>> neg_zero = -0.0
>>> neg_zero

This may be a weird behavior in some situations. So, you may need to remove the negative sign. To do this, you can use the z option:

>>> neg_zero = -0.0
>>> f"{neg_zero:z.4f}"

In this example, you use the z option to coerce the negative zero to positive zero. Note that this only works for fixed-point representation types, f or F. Also, the z option appends zeros to the output to fill the intended precision. In the example, you use a precision of 4, so you get four zeros after the decimal point.

Another useful option that you’ll find in the format specifier syntax is the # modifier. This option allows you to present numeric values using their alternate form. Different numeric data types have different alternate forms. The following table summarizes what the # modifier does in each data type:

Numeric Type Result of Using "#"
int Adds the prefix 0b, 0o, 0x, or 0X to binary, octal, hexadecimal, and capital-hexadecimal notations, respectively
float and complex Adds the decimal-point character to the end of the value even if no digit follows it

As you can see, the alternate form modifier (#) impacts the representation of numeric types in different ways. To understand how this modifier works, consider the following examples:

>>> number = 42

>>> f"hex: {number:#x};  oct: {number:#o};  bin: {number:#b}"
'hex: 0x2a;  oct: 0o52;  bin: 0b101010'

>>> f"float: {number:#.0f}"
'float: 42.'

In the first example, you use different integer variations to represent a number. The # specifier adds the appropriate prefix to the output.

In the second example, you represent a number as a floating-point value. In this case, you have a precision of 0, meaning that you don’t want to display decimal values. However, the # modifier appends the decimal point, so anybody can know that this is a floating-point value.


Now you know how to create format specifiers to format the values that you interpolate into your strings using Python’s format mini-language. A format specifier is a string that follows a syntax defined in this mini-language. With a proper format specifier, you can format numbers as currency values, use scientific notation, express a value as a percentage, and more.

In this tutorial, you’ve learned how to:

  • Use the format mini-language syntax in your strings
  • Align and fill textual output in your Python code
  • Convert between data types in your outputs
  • Provide formatting fields dynamically using variables and expressions
  • Format numeric values in different ways to solve different problems

With this knowledge, you’re now ready to start creating nicely formatted strings and messages in your Python code.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.

Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning

Related Topics: intermediate python