16. Formatted Output and Some Text String Manipulation

This work is licensed under Creative Commons Attribution-ShareAlike 4.0 International


16.1. The function print()

The basic tool for displaying information from Python to the screen is the function print(), as in

print("Hello world.")
Hello world.

What this actually does is convert one or more items given as its input arguments to strings of text, and then displays them on a line of output, each separated from the next by a single space. So it also handles numbers:

print(7)
print(1/3)
7
0.3333333333333333

and variables or expressions containing text strings, numbers or other objects:

x = 1
y = 2
print(x,"+",y,"=",x+y)
1 + 2 = 3
hours = [7, 11]
claim = "The hours of opening of our stores are"
print(claim, hours)
print("That is, from", hours[0], "am to", hours[1], "pm.")
The hours of opening of our stores are [7, 11]
That is, from 7 am to 11 pm.

16.2. F-string formatting

When assembling multiple pieces of information, the above syntax can get messy; also, it automatically inserts a blank space between items, so does not allow us to avoid the space before “am” and “pm”.

Python has several methods for finer string manipulation; my favorite is f-strings, introduced in Python version 3.6, so I will describe only it. (The two older approaches are the “%” operator and the .format() method; if you want to know about them — if only for a reading knowledge of code that uses them — there is a nice overview at https://realpython.com/python-string-formatting/)

The key idea of f-strings is that when an “f” is added immediately before the opening quote of a string, parts of that string within braces {…} are taken as input which is processed and the results inserted into a modifed string. For example, the previous print command above could instead be done with precise control over blank spaces as follows:

print(f"That is, from {hours[0]}am to {hours[1]}pm.")
That is, from 7am to 11pm.

Actually, what happens here involves two steps:

  1. The “f-string” is converted to a simple, literal string.

  2. function print(...) then prints this literal string, as it would with any string.

Thus we can do things in two parts:

hours_of_operation = f"Our stores are open from {hours[0]}am till {hours[1]}pm."
print(hours_of_operation)
Our stores are open from 7am till 11pm.

Sometimes this is useful, because strings get used in other places besides print(), such as the titles and labels of a graph, and as we will see next, it can be convenient to assemble a statement piece at a time and then print the whole thing; for this, we use “addition” of strings, which is concatenation.

Note also the explicit insertion of spaces and such.

q1 = "When do stores open?"
a1 = f"Stores open at {hours[0]}am."
q2 = "When do stores close?"
a2 = f"Stores close at {hours[1]}pm."
faq = q1+' '+a1+' — '+q2+' '+a2
print(faq)
When do stores open? Stores open at 7am. — When do stores close? Stores close at 11pm.

16.3. Formatting numbers

The information printed above came out alright, but there are several reasons we might want finer control over the display, especially with real numbers (type float)

  • Choosing the number of significant digits or correct decimals to display for a float (real number).

  • Controlling the width a number’s display (for example, in order to line up columns).

First, width control. The following is slightly ugly due to the shifts right.

for exponent in range(11):
    print(f"4^{exponent} = {4**exponent}")
4^0 = 1
4^1 = 4
4^2 = 16
4^3 = 64
4^4 = 256
4^5 = 1024
4^6 = 4096
4^7 = 16384
4^8 = 65536
4^9 = 262144
4^10 = 1048576

To line things up, we can specify that each output item has as many spaces as the widest of them needs: 2 and 7 columns respectively, with syntax{quantity:width}

for exponent in range(11):
    print(f"4^{exponent:2} = {4**exponent:7}")
4^ 0 =       1
4^ 1 =       4
4^ 2 =      16
4^ 3 =      64
4^ 4 =     256
4^ 5 =    1024
4^ 6 =    4096
4^ 7 =   16384
4^ 8 =   65536
4^ 9 =  262144
4^10 = 1048576

That is still a bit strange with the exponents, because the output is right-justified. Left-justified would be better, and is done with a “<” before the width. (As you might guess, “>” can be used to explicitly specify right-justification).

for exponent in range(11):
    print(f"4^{exponent:<2} = {4**exponent:>7}")
4^0  =       1
4^1  =       4
4^2  =      16
4^3  =      64
4^4  =     256
4^5  =    1024
4^6  =    4096
4^7  =   16384
4^8  =   65536
4^9  =  262144
4^10 = 1048576

Next, dealing with float (real) numbers: alignment, significant digits, and scientific notation vs fixed decimal form.

Looking at:

for exponent in range(11):
    print(f"4^-{exponent:<2} = {1/4**exponent}")
4^-0  = 1.0
4^-1  = 0.25
4^-2  = 0.0625
4^-3  = 0.015625
4^-4  = 0.00390625
4^-5  = 0.0009765625
4^-6  = 0.000244140625
4^-7  = 6.103515625e-05
4^-8  = 1.52587890625e-05
4^-9  = 3.814697265625e-06
4^-10 = 9.5367431640625e-07

… we see several things:

  • real numbers are by default left justified, as opposed to the default right justification of integers.

  • some automatic decision is made about when to use scientific notation, where the part after “e” is the exponent (base 10). [Roughly the shorter of the two options is used.]

  • The number of significant digits can be excessive: the full 16 decimal place precision of 64-bit numbers is often not needed or wanted.

These can be adjusted. Justification was already mentioned, so the next new feature is an optional letter after the width value, to specify more about how to format the data:

  • ‘d’ specifies an integer (but this is the default when the value is known to be of type int)

  • ‘f’ specifies fixed width format for a float; no exponent

  • ‘e’ specifies scientific notation format for a float; with exponent

  • ‘g’ specifies that a float is formatted either ‘f’ or ‘e’, based on criteria like which conveys the most precision in a given amount of space.

All three format specifiers for type float also impose a lower limit on how many digits are displayed; about seven:

for exponent in range(11):
    print(f"4^-{exponent:<2d} = {1/4**exponent:f}")
4^-0  = 1.000000
4^-1  = 0.250000
4^-2  = 0.062500
4^-3  = 0.015625
4^-4  = 0.003906
4^-5  = 0.000977
4^-6  = 0.000244
4^-7  = 0.000061
4^-8  = 0.000015
4^-9  = 0.000004
4^-10 = 0.000001
for exponent in range(11):
    print(f"4^-{exponent:<2} = {1/4**exponent:e}")
4^-0  = 1.000000e+00
4^-1  = 2.500000e-01
4^-2  = 6.250000e-02
4^-3  = 1.562500e-02
4^-4  = 3.906250e-03
4^-5  = 9.765625e-04
4^-6  = 2.441406e-04
4^-7  = 6.103516e-05
4^-8  = 1.525879e-05
4^-9  = 3.814697e-06
4^-10 = 9.536743e-07
for exponent in range(11):
    print(f"4^-{exponent:<2} = {1/4**exponent:g}")
4^-0  = 1
4^-1  = 0.25
4^-2  = 0.0625
4^-3  = 0.015625
4^-4  = 0.00390625
4^-5  = 0.000976562
4^-6  = 0.000244141
4^-7  = 6.10352e-05
4^-8  = 1.52588e-05
4^-9  = 3.8147e-06
4^-10 = 9.53674e-07

To control precision, the width specifier gains a “decimal” p, with {...:w.p} specifying p decimal places or significant digits, depending on context. Also, the width can be omitted if only precision matters, not spacing.

Let’s ask for 9 digits:

for exponent in range(11):
    print(f"4^-{exponent:<2d} = {1/4**exponent:.9f}")
4^-0  = 1.000000000
4^-1  = 0.250000000
4^-2  = 0.062500000
4^-3  = 0.015625000
4^-4  = 0.003906250
4^-5  = 0.000976562
4^-6  = 0.000244141
4^-7  = 0.000061035
4^-8  = 0.000015259
4^-9  = 0.000003815
4^-10 = 0.000000954
for exponent in range(11):
    print(f"4^-{exponent:<2} = {1/4**exponent:.9e}")
4^-0  = 1.000000000e+00
4^-1  = 2.500000000e-01
4^-2  = 6.250000000e-02
4^-3  = 1.562500000e-02
4^-4  = 3.906250000e-03
4^-5  = 9.765625000e-04
4^-6  = 2.441406250e-04
4^-7  = 6.103515625e-05
4^-8  = 1.525878906e-05
4^-9  = 3.814697266e-06
4^-10 = 9.536743164e-07

Putting it all together, here are width and precision specified.

Note that text string pieces can also have width specifications; for example, to align columns:

print(f"{'':8}{'fixed point':15}{'scientific notation':16}")
for exponent in range(11):
    print(f"4^-{exponent:<2} = {1/4**exponent:11.9f}, = {1/4**exponent:0.9e}")
        fixed point    scientific notation
4^-0  = 1.000000000, = 1.000000000e+00
4^-1  = 0.250000000, = 2.500000000e-01
4^-2  = 0.062500000, = 6.250000000e-02
4^-3  = 0.015625000, = 1.562500000e-02
4^-4  = 0.003906250, = 3.906250000e-03
4^-5  = 0.000976562, = 9.765625000e-04
4^-6  = 0.000244141, = 2.441406250e-04
4^-7  = 0.000061035, = 6.103515625e-05
4^-8  = 0.000015259, = 1.525878906e-05
4^-9  = 0.000003815, = 3.814697266e-06
4^-10 = 0.000000954, = 9.536743164e-07

Another observation: the last text string ‘scientific notation’ was too long for the specified 16 columns, and the “e” format for the second version of the power definitely needed more than the 0 columns requested. So they went over, rather than getting truncated — the width specification is just a minimum.

16.3.1. Further reading, and a P. S.:

  • https://realpython.com/python-f-strings/

  • As always, consider the function help(), as below

help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

By default a single space is inserted between items.

You can change that with the optional argument “sep”:

print(1,"+",1,'=',1+1,sep="")
1+1=2
print(1,"+",1,'=',1+1,sep=" ... ")
1 ... + ... 1 ... = ... 2

Also, by default the output line is ended at the end of a print command; technically, an “end of line” character “\n” is added at the end of the output string.

One can change this with the optional argument “end”, for example to allow a single line to be assembled in pieces, or to specify double spacing.

No new line after print:

for count_to_10 in range(1, 11):
    print(count_to_10,sep="",end="")
    if count_to_10 < 10:  # there is more to come after this item
        print(" ... ",sep="",end="")
    else: print()  # end the line at last
1 ... 2 ... 3 ... 4 ... 5 ... 6 ... 7 ... 8 ... 9 ... 10

Double spacing:

for count_to_10 in range(1, 11):
    print(count_to_10,end="\n\n")
1

2

3

4

5

6

7

8

9

10

Return to the top