Python has multiple mechanisms to format strings, allowing to intertwine static text with the contents of variables or expressions.
Python 3.8 introduced formatted string literals, also called as f-strings due its f prefix before the string quote, which has similar functionality to the string interpolation present in PHP and recent versions of JavaScript.
However, Python's f-strings are powered by a mini-language, allowing to also define precision, alignment and notation, which isn't possible without the assistance of functions in PHP or JavaScript.
The old c-style sprintf mechanism is still possible, although it's preferable to use the new way to format strings since the old way might be in the future deprecated and it was replaced with f-strings due its limitations.

An f-string is any quoted, single or double, string prefixed with an f.
In this case, python will interpret the content inside of a curly brackets as a variable or expression that will be computed and its content will replace the string section.

>>> item = "ACME"
>>> price = 10.5
>>> quantity = 4
>>> print(f"If I buy {quantity} {item} stocks, it will cost me {price*quantity}")
If I buy 4 ACME stocks, it will cost me 42.0

While item variable is textual, quantity is numerical, and price * quantity is a mathematical expression that will output a number.
Arrays, tuples and dictionaries can also be used in f-strings:

>>> q_earnings = [100, 150, 120]
>>> print(f"On the 3rd quarter, it was earned {q_earnings} with a total of {sum(q_earnings)}")
 On the 3rd quarter, it was earned [100, 150, 120] with a total of 370

>>> send_to = {"address": "Acme Avenue", "zip": 555 }
>>> print(f" Send letter to {send_to}")
 send letter to {'address': 'Acme Avenue', 'zip': 555}

>>> print(f" Mailing Address:\n        {send_to['address']}\n        {send_to['zip']}")
 Mailing Address:
        Acme Avenue
        555

Alignment and fixed-point notation in f-strings

In the presence of a numerical expression, python will convert it to a number using general notation.

>>> bacteria_growth = {"January": 0.00001, "February": 120.56, "March": 1560.67 }
>>> for index, month in enumerate(bacteria_growth):
        print(f"| {month} | {bacteria_growth[month]} |")
| January | 1e-05 |
| February | 120.56 |
| March | 1560.67 |

If we want to present data in a pretty formatted table, the first step is to force the fixed-point notation, in order to avoid the 0.00001 to be represented as 1e-05.
The fixed-point notation is defined by adding a : followed by f, representing fixed-point notation. In this case bacteria_growth[month] becomes bacteria_growth[month]:f.

>>> bacteria_growth = {"January": 0.00001, "February": 120.56, "March": 1560.67 }
>>> for index, month in enumerate(bacteria_growth):
        print(f"| {month} | {bacteria_growth[month]:f} |")
| January | 0.000010 |
| February | 120.560000 |
| March | 1560.670000 |

Python aligns the fields to the left by default, except for numbers which are aligned to the right. To align the fields, set enough space in order for all the letters and digits to fit the space. This is done by adding the “width” of the field after the semicolon and before the notation.
In this case bacteria_growth[month] becomes bacteria_growth[month]:11f, and month becomes month:8.

>>> bacteria_growth = {"January": 0.00001, "February": 120.56, "March": 1560.67 }
>>> for index, month in enumerate(bacteria_growth):
>>> for index, month in enumerate(bacteria_growth):
        print(f"| {month:8} | {bacteria_growth[month]:11f} |")
| January  |    0.000010 |
| February |  120.560000 |
| March    | 1560.670000 |

Alignment direction in in f-strings

The default alignment can be overridden, which is right for numbers and left for other data types, by adding after semicolon:

  • < -  for left alignment.
  • > - for right alignment.
  • ^ - to the center.
  • = - to keep the - sign to the left, and then align the rest of the numerical field to the right.

>>> sales = {"January": 156, "February": 65, "March": 90}
>>> costs = {"January": 20, "February": 165, "March": 10}
>>> sum = 0
>>> for index, month in enumerate(sales):
        print(f"| {month:^8} | " + # aligned to the center
              f"{sales[month]:<4} | " + # aligned to the left
              f"{'Loss' if sales[month] < costs[month] else 'Profit':>8} | " + # aligned to the right
              f" {sales[month] - costs[month]:=8} |") # negative sign to the left, the rest to the right
| January  | 156  |   Profit |        136 |
| February | 65   |   Loss   |  -     100 |
|  March   | 90   |   Profit |         80 |

Decimal precision in f-strings

The number of decimal places can be rounded or right padded with zeros by defining the formatter after the semicolon with the following format:
number:[width][.precision][notation]

  • width is the total number of characters that will be displayed including the minus sign, integral part, the dot and the decimal part.
    This field is optional, in such cases the total number of characters is variable.
  • .precision:
            - for fixed-point notation is the total digits before and after the dot.
            - for general notation is the total number of digits after the dot.
  • notation - defines the notation to be outputted.
            - g for general notation.
            - f for fixed-point notation.

###  Using fixed-point notation  ###
# the width is 10, therefore 10 characters will be displayed with right alignment
# the precision is 4, therefore 4 digits after the dot will be displayed
>>> print(f"[{123.4567891:10.4f}]\n[1234567890]")
[  123.4568]
[1234567890]

# the zero before the 10th, will fill empty left space with zeros
>>> print(f"[{123.4567891:010.4f}]\n[1234567890]")
[00123.4568]
[1234567890]

# the negative sign will be displayed within the 10 characters
>>> print(f"[{-123.4567891:10.4f}]\n[1234567890]")
[ -123.4568]
[1234567890]

##  Using general notation  ###
# the precision is still 4, but in general notation
#  it represents the sum of all the digits (3+1=4)
>>> print(f"[{123.4567891:10.4}]\n[1234567890]")
[     123.5]
[1234567890]

Although, width field defines the total number of characters, the output can be larger than this number.

# Although the width is 10, it needs 11 characters to accommodate 10 digits plus the dot
>>> print(f"[{123.4567891:10.10}]\n[1234567890]")
[123.4567891]
[1234567890]

# It needs 12 characters to accommodate 10 digits plus the dot and minus sign
>>> print(f"[{-123.4567891:10.10}]\n[1234567890]")
[-123.4567891]
[1234567890]