Fancy Printing With Python

Posted on 10-11-17 in Computing

Review of the basics

The first program we all ever write is simply

print("Hello World!")

Most of the time this is good enough. If we want we can pass multiple things to our print function including variables

name = "Mark"
number_of_apples = 9
print("Hello", name, "I hope you are well! Apparently you have", number_of_apples, "apples")

If we want to write more complex print statements we should use formatting. In Python there are now three ways of doing this. In this article I'm going to focus on the modern approach using F Strings. NOTE! This will only work on Python3.6 and later.

For example let's rewrite the above with the format

name = "Mark"
number_of_apples = 9

some_text = f"Hello {name} I hope you are well! Apparently you have {number_of_apples} apples."

Here Python will replace every instance of {} with the corresponding item inside the format brackets. We can pass any valid Python in these arguments such as

a = 5
b = 6
print(f'{a} x {b} = {a*b}')

Importantly we can also specify arguments inside each pair of curly braces. For instance

details = (('roger', 20), ('mark', 31), ('jeff', 5), ('mary', 101))

for name, age in details:
  print(f"{name:20} is {age:3} years old")

will output

  roger                is  20 years old
  mark                 is  31 years old
  jeff                 is   5 years old
  mary                 is 101 years old

Notice how whitespace has been preserved giving everything a rather tidy look! Here we specify the minimum whitespace to give by using a colon followed by a number. Consider another example

some_number = 122.3334444455
print(f"You have £{some_number:.2f}"  # For example, if it's money

will output

>>> You have £122.33

For example if we want to print a number out in different bases we can simply use

number = 65

format_options = (('Float', 'f'), ('Decimal', 'd'), ('Binary', 'b'), ('Octal', 'o'), ('Hex', 'x'), ('ASCII', 'c'))

for name, mode in format_options:
    print(f"{name:10} {number:{mode}}")

will give

  Float   65.000000
  Decimal 65
  Binary  1000001
  Octal   101
  Hex     41

aside Why do you think we have used curly braces inside other curly braces above?

As you can see this becomes rather useful if we know what format code to use! To understand more format patterns have a look at

Using Format to simplify our code

When we want a lot of information from a user we can often find ourselves writing an input again and again. Consider asking for information about a parcel. We will store our data in a dictionary. We can just do the following

parcel_id = int(input("please enter the parcel id >> "))
length = float(input("Please enter the length >> "))
width = float(input("Please enter the width >> "))
height = float(input("Please enter the height >> "))
weight = float(input("Please enter the weight >> "))

new_parcel = {'id'   : parcel_id,
            'length' : length,
            'width'  : width,
            'height' : height,
            'weight' : weight}

Just by looking we can see this is very repetitive. What we want to to do is share the bits of the code which repeat and insert the changes as needed.

parcel_format = (('parcel id', int),
                 ('length' , float),
                 ('width' , float),
                 ('height' , float),
                 ('weight' , float),
                 ('address' , str))

new_parcel = {}

for key, field_type in parcel_format:
    question_to_ask = f"Please enter the {key} >> "
    new_parcel[key] = field_type(input(question_to_ask))

Here we have used the format to alter the question being asked each time. Note that the field_type is also changing per data type. Combine this format with input validation will allow us to produce clean code to ask user for data. See my post on asking for validated input for further information.

One possible implementation using validated input is given below

def ask_for_input(key, field_type, lower, upper):
    Asks for valid input from user
    Ensures value is within inclusive range of lower and upper
    if lower==upper then no range needed

    while True:
        question_to_ask = f"Please enter the {key} >> "
            answer = field_type(input(question_to_ask))
        except ValueError as e:
            print(f"{e} - Invalid format for {key}")

        if lower == upper or answer >= lower and answer <= upper:
            return answer
            print(f"{answer:10} is outside range {lower:3} to {upper:3} for {key}")

parcel_format = (('parcel id', int, 0, 1000),
                 ('length' , float, 0, 100),
                 ('width' , float,  0, 100),
                 ('height' , float, 0, 100),
                 ('weight' , float, 0, 100),
                 ('address' , str, 0, 0))

new_parcel = {key : ask_for_input(key, *args) for key, *args in parcel_format}

Here I've added a lower and upper limit to each piece of data we wish to ask for. Note that the final line makes use of dictionary comprehensions and argument unpacking. Here's some example input

  Please enter the parcel id >> -1
          -1 is outside range   0 to 1000 for parcel id
  Please enter the parcel id >> apple
  invalid literal for int() with base 10: 'apple' - Invalid format for parcel id
  Please enter the parcel id >> 1
  Please enter the length >> 20
  Please enter the width >> 25
  Please enter the height >> 30
  Please enter the weight >> 200
       200.0 is outside range   0 to 100 for weight
  Please enter the weight >> 1 Fleet St, London
  could not convert string to float: '1 Fleet St, London' - Invalid format for weight
  Please enter the weight >> 80
  Please enter the address >> 1 Fleet St, London

This gives us a single dictionary entry

{'parcel id': 1, 'length': 20.0, 'width': 25.0, 'height': 30.0, 'weight': 80.0, 'address': '1 Fleet St, London'}


  1. Extent the above code to ask for multiple parcels. Reject existing ids.
  2. Label parcels based on their density (< 1000 will float, > 1000 will sink).


F-strings are very powerful and allow us to produce clean output quickly.