Part 2 - growing a program step by step: a restaurant bill calculator

Had WillHad Will
20 min read

« Previous article

Next article »

While this article touches upon many subjects, it is really about the process of creating a program iteratively. When starting with an idea for a program, we have to start from somewhere. Try to see how the code evolves through the article; this might help you avoid blank page syndrome in the future.

Today, we’ll create a simple program to calculate tips, deduct discounts, and round and split restaurant bills.

Version 1: Program outline

I like to start a program with a general outline. It doesn’t really do anything yet: I delineated with comments where and how the program’s functionality should be laid out. Each comment corresponds to a major part of the program. I’ll fill them one by one. This is a dynamic process: I might change my mind and rearrange the code’s structure along the way.

def main():
    # Get input
    # Calculate tip
    # Display results
    pass

if __name__ == '__main__':
    main()

First, We’ll get relevant inputs from the user and store them in variables. We’ll use those variables for our calculations, and then we’ll finally nicely display the results.

pass is a special statement telling the Python interpreter to do nothing. You cannot define a function that contains no code. pass prevents any error before we start writing actually useful lines of code. Try to delete the pass statement to see how the Python interpreter reacts.

Version 2: Getting input

I want to start with inputs. We’ll first write a naïve version and refine it later. I first begin by creating two variables: price and tip_percent. The price should be a floating point number (dollars and cents), while you usually tip a whole number of percent.

When I write code to get user input, I like to include a few lines of print statements that examine the data. It displays the variable’s contents and their type. This is useful so we know we’re good before using those variables in further calculations. Those lines can either be deleted or commented out. Whichever results in the clearest code.

def main():
    # Get input
    price = float(input("How much does your meal cost? "))
    tip_percent = int(input("How much do you want to tip in percents? "))
    # temporary code to examine our data
    print(f"price: ${price}")
    print(f"price type {type(price)}")
    print(f"tip_percent: {tip_percent}%”)
    print(f"tip_percent type: {type(tip_percent)}")
    # Calculate tip

    # Display results

    pass


if __name__ == '__main__':
    main()
% python3 Project2.py
How much does your meal cost? 54
How much do you want to tip in percents? 2
price: 54.0
price type <class 'float'>
tip_percent: 2
tip_percent type: <class 'int'>

Our program works relatively well for regular inputs. The output shows how the Python interpreter displays an object’s type. This can be lifesaving info when you’re debugging your code.

Now, we’ll try some bad inputs. You should always try to break your program to make it as robust as possible (or as robust as time and motivation permit).

python3 Project2.py
How much does your meal cost? tkyf
Traceback (most recent call last):
  File "~/Project2.py", line 18, in <module>
    main()
  File "~/Project2.py", line 3, in main
    price = float(input("How much does your meal cost? "))
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: ‘tkyf'

The Python interpreter tells us exactly why and where our code went wrong.

Version 3: basic input validation

We’ll add some basic input validation in our next version. Since this is not the focus of this post, we’ll keep it very simple (and quite incomplete). You’ll see in a later post how to make it more robust. If you’re perplexed by the code below, you’ll find helpful info in my last post. Please go read it!

def main():
    # Get input
    price = input("How much does your meal cost? ").strip()
    if price:
        price = float(price)
    else:
        print("Error: the price is not right.")
        return
    tip_percent = input("How much do you want to tip in percents? ").strip()
    if tip_percent:
        tip_percent = int(tip_percent)
    else:
        print("Error: tip_percent is not right")
        return
    # temporary code to examine our data
    print(f"price: {price}")
    print(f"price type {type(price)}")
    print(f"tip_percent: {tip_percent}")
    print(f"tip_percent type: {type(tip_percent)}")
    # Calculate tip

    # Display results

    pass


if __name__ == '__main__':
    main()

This version tests for missing inputs and converts only existing inputs to floats and ints. Here, we see the same if/else statements we saw in the last post. If our code executes an else block, it hits a return statement and exits the main function (and thus exits the whole program.)

% python3 Project2.py
How much does your meal cost? 120
How much do you want to tip in percents? 10
price: 120.0
price type <class 'float'>
tip_percent: 10
tip_percent type: <class ‘int'>

Try various types of input (e.g., integers, letters, nothing, floats) and study the output.

Version 4: tip calculations

Now we know we’ve got some inputs to work with, we can use them to do our first calculations.

def main():
    # Get input
    price = input("How much does your meal cost? ").strip()
    if price:
        price = float(price)
    else:
        print("Error: the price is not right.")
        return
    tip_percent = input("How much do you want to tip in percents? ").strip()
    if tip_percent:
        tip_percent = int(tip_percent)
    else:
        print("Error: tip_percent is not right")
        return
    # temporary code to test what we've just written
    print(f"price: {price}")
    print(f"price type {type(price)}")
    print(f"tip_percent: {tip_percent}")
    print(f"tip_percent type: {type(tip_percent)}")
    # Calculate tip
    tip = price * tip_percent / 100
    print(f"tip: {tip}")
    price_with_tip = price + tip
    print(f"price_with_tip: {price_with_tip}")
    # Display results

    pass


if __name__ == '__main__':
    main()

Our calculations are two variable assignments: tip and price_with_tip. As you see, calculations use previously defined variables as if they were numbers.

Python lets you use +, *, and / as you would in math.

Before creating a clean formatted output, I like to write quick and dirty print statements to ensure our calculations are correct. If there’s a mistake, we can catch it without much effort.

Version 5: tip display

def main():
    # Get input
    price = input("How much does your meal cost? ").strip()
    if price:
        price = float(price)
    else:
        print("Error: the price is not right.")
        return
    tip_percent = input("How much do you want to tip in percents? ").strip()
    if tip_percent:
        tip_percent = int(tip_percent)
    else:
        print("Error: tip_percent is not right")
        return

    # Calculate tip
    tip = price * tip_percent / 100
    price_with_tip = price + tip

    # Display results
    print(f"\tYour tip: {tip}\n\tPrice with tip: {price_with_tip}")


if __name__ == '__main__':
    main()
% python3 Project2.py
How much does your meal cost? 100
How much do you want to tip in percents? 15
    Your tip: 15.0
    Price with tip: 115.0

This is the occasion to introduce escape characters. As you see our program output, there’s a tab in front of the tip and the price with the tip. There’s also a new line between the two. Looking closely at our f-string responsible for output, you’ll notice two strange elements: \t and \n. Those are escape sequences.

Escape sequences let you add “forbidden” or special characters. For example, if you wanted to add one quote character inside a string, the Python interpreter would be confused: does this quote mean this is the end of the string, or is it a quote character? To indicate that a quote is a character, use \" or \'.

Escape sequences start with a backslash.

Here are the most used escape characters in Python:

Escape character
\<newline>Ignores the backslash and the newline
\\Introduce a backslash
\'Single quote
\"Double quote
\nNewline
\tTab
\rWindows-style carriage return (useful in certain file-based cases)

Try using these escape characters in our program’s formatted output. Try combining them.

Version 6: Including discounts

Now, let’s make our program better: users can input a value that will be discounted from the price with a tip.

It will require asking the user a new question and adding one calculation. We will separate these two actions: asking the user for input will be next to our preexisting price and tip percentage variables. The discount calculation goes with the tip calculation. And we display all the results together. Code asking for input goes together, code doing calculations goes together, and code creating output goes together.

def main():
    # Get input
    price = input("How much does your meal cost? ").strip()
    if price:
        price = float(price)
    else:
        print("Error: the price is not right.")
        return
    tip_percent = input("How much do you want to tip in percents? ").strip()
    if tip_percent:
        tip_percent = int(tip_percent)
    else:
        print("Error: tip_percent is not right")
        return
    discount_percent = input("Discount percentage from base price (if any, else 0): ").strip()
    if discount_percent:
        discount_percent = float(discount_percent)
    else:
        discount_percent = 0

    # Calculate tip
    tip = price * tip_percent / 100
    price_with_tip = price + tip

    # Calculate discount on price with tip_percent
    discount_amount = price * discount_percent / 100
    discounted_price = price_with_tip - discount_amount

    # Display results
    print(f"""
    Summary:
        Base price:           {price}
        Tip percentage:       {tip_percent}
        Discount percentage:  {discount_percent}
        Tip amount:           {tip}
        Price with tip:       {price_with_tip}
        Discount amount:      {discount_amount}
        Discounted price:     {discounted_price}
    """)


if __name__ == '__main__':
    main()

It’s not guaranteed that a user has a discount for his meal. It would be annoying to input a zero if the user doesn’t have it. So, we want to equate the absence of input with the number 0. This is done with a simple if/else branch.

In Python, triple quotes let you create multi-line strings. You can see below that we can create pretty output. Summary is indented to the right of our three input questions. Can you guess how it will be printed? Try to make it indented more to the right. What would the output look like if we had used separate print statements for every output line (price, tip_percent, discount_percent, tip, etc.)?

% python3 Project2.py
How much does your meal cost? 100
How much do you want to tip in percents? 15
Discount percentage (if any, else 0): 

    Summary:
        Base price:           100.0
        Tip percentage:       15
        Discount percentage:  0
        Tip amount:           15.0
        Price with tip:       115.0
        Discount amount:      0.0
        Discounted price:     115.0

Version 7: Splitting the bill

Our program is becoming quite big. It is an excellent time to create some structure.

We’ll also add the possibility of splitting the bill between different customers.

def main():
    #############################################
    # Get input
    #############################################

    # Get price
    price = input("How much does your meal cost? ").strip()
    if price:
        price = float(price)
    else:
        print("Error: the price is not right.")
        return

    # Get tipping percentage
    tip_percent = input("How much do you want to tip in percents? ").strip()
    if tip_percent:
        tip_percent = int(tip_percent)
    else:
        print("Error: tip_percent is not right")
        return

    # Get discount percentage
    # Discount gets applied to base price and doesn't affect tipping
    discount_percent = input("Discount percentage from base price (if any, else 0): ").strip()
    if discount_percent:
        discount_percent = float(discount_percent)
    else:
        discount_percent = 0

    # Get bill splitting info
    number_of_splitters = 1
    splitting_p = input("Will you split the bill? (yes or no): ").strip().lower()
    if splitting_p and (splitting_p == 'yes' or splitting_p == 'y'):
        number_of_splitters = input("How many people to split the bill? ").strip()
        number_of_splitters = int(number_of_splitters)

    #############################################
    # Calculations
    #############################################

    # Calculate tip
    tip = price * tip_percent / 100
    price_with_tip = price + tip

    # Calculate discount on price with tip_percent
    discount_amount = price * discount_percent / 100
    discounted_price = price_with_tip - discount_amount

    #Split the bill
    amount_per_person = discounted_price / number_of_splitters

    #############################################
    # Display results
    #############################################

    print(f"""
    Summary:
        Base price:           {price}
        Tip percentage:       {tip_percent}
        Discount percentage:  {discount_percent}
        Tip amount:           {tip}
        Price with tip:       {price_with_tip}
        Discount amount:      {discount_amount}
        Discounted price:     {discounted_price})
        Amount per person:    {amount_per_person:.2f}""")


if __name__ == '__main__':
    main()

We get the number of people splitting the bill by creating a base number of customers (number_of_splitters) of 1 (only one customer). Then, we ask the user if he wants to split the bill. If he types yes or y (or a variation with capital letters), we ask for a number and modify number_of_splitters accordingly.

It’s a good idea to have a value for number_of_splitters, whether the bill will be split. If we didn’t do this, we would have to test for splitting when we do the calculations and when we display the results. You can try to remove the number_of_splitters = 1 line and modify the rest of the code to get the program to work (Hint: you’ll have to add if/else branching when doing calculations and displaying the results.)

If you look at the f-string, you’ll notice amount_per_person is followed by “:.2f”. In Python, you can control how floats look by appending a colon (:), followed by a format specifier. Below are some more ways to use format specifiers:

SpecifierexampleDescription
.nff"{value:.2f}"Round value to n decimal places
E or ef"{value:.2e}"Display value in scienctific notation
G or gf"{value:.4g}"Use fixed-point or scientific notation, depending on the magnitude
nf"{value:.2n}"Like .2f but uses the locale settings for number formatting (eg, a dot in the US, a comma in Europe, etc)
,f"{value:,.2f}"Use comma as the separator for thousands
%f"{value:.2f%}"Convert the number to a percentage.
\>nf"{value:>10.2f}"align to the right of a n-character wide field

Try the examples above and play with them to see how the specifiers behave.

Below is how the program behaves on my system.

python3 Project2.py
How much does your meal cost? 100
How much do you want to tip in percents? 15
Discount percentage from base price (if any, else 0): 5
Will you split the bill? (yes or no): y
How many people to split the bill? 7

    Summary:
        Base price:           100.0
        Tip percentage:       15
        Discount percentage:  5.0
        Tip amount:           15.0
        Price with tip:       115.0
        Discount amount:      5.0
        Discounted price:     110.0
        Amount per person:    15.71

What happens if you try entering a negative number for the discount or the number of customers splitting the bills? Modify the code to ensure the input is a positive number.

Version 8: paying in cash

Let’s add new functionality to our program: we’ll provide the user a way to pay his bill in cash: amount_per_person will be divided between $100 bills, $50 bills, etc., down to one cent coins. I did not transcribe the whole program for brevity, as it would take a lot of space. This version of our program takes a naïve approach to finding the amount of each bill and coin type. We go from the biggest denomination to the smallest in order.

    #############################################
    # Calculations
    #############################################

    # Calculate tip
    tip = price * tip_percent / 100
    price_with_tip = price + tip

    # Calculate discount on price with tip_percent
    discount_amount = price * discount_percent / 100
    discounted_price = price_with_tip - discount_amount

    # Split the bill
    amount_per_person = discounted_price / number_of_splitters

    # Pay with cash
    left_to_pay = amount_per_person
    hundred_notes = int(left_to_pay // 100)
    left_to_pay = left_to_pay % 100
    fifty_notes = int(left_to_pay // 50)
    left_to_pay = left_to_pay % 50
    twenty_notes = int(left_to_pay // 20)
    left_to_pay = left_to_pay % 20
    ten_notes = int(left_to_pay // 10)
    left_to_pay = left_to_pay % 10
    five_notes = int(left_to_pay // 5)
    left_to_pay = left_to_pay % 5
    one_notes = int(left_to_pay // 1)
    left_to_pay = left_to_pay % 1
    quarters = int(left_to_pay // 0.25)
    left_to_pay = left_to_pay % 0.25
    dimes = int(left_to_pay // 0.1)
    left_to_pay = left_to_pay % 0.1
    nickels = int(left_to_pay // 0.05)
    left_to_pay = left_to_pay % 0.05
    singles = int(left_to_pay // 0.01)


    #############################################
    # Display results
    #############################################

    print(f"""
    Summary:
        Base price:           {price}
        Tip percentage:       {tip_percent}
        Discount percentage:  {discount_percent}
        Tip amount:           {tip}
        Price with tip:       {price_with_tip}
        Discount amount:      {discount_amount}
        Discounted price:     {discounted_price}
        Amount per person:    {amount_per_person:.2f}

    Pay with cash: {hundred_notes} hundred $ bills
                   {fifty_notes} fifty $ bills
                   {twenty_notes} twenty $ bills
                   {ten_notes} ten $ bills
                   {five_notes} five $ bills
                   {one_notes} one $ bills
                   {quarters} quarters
                   {dimes} dimes
                   {nickels} nickels
                   {singles} singles
    """)
% python3 Project2.py
How much does your meal cost? 347.56
How much do you want to tip in percents? 15
Discount percentage from base price (if any, else 0): 2
Will you split the bill? (yes or no): y
How many people to split the bill? 4

    Summary:
        Base price:           347.56
        Tip percentage:       15
        Discount percentage:  2.0
        Tip amount:           52.13399999999999
        Price with tip:       399.694
        Discount amount:      6.9512
        Discounted price:     392.7428
        Amount per person:    98.19

    Pay with cash: 0 hundred $ bills
                   1 fifty $ bills
                   2 twenty $ bills
                   0 ten $ bills
                   1 five $ bills
                   3 one $ bills
                   0 quarters
                   1 dimes
                   1 nickels
                   3 singles

We made heavy use of the % and // operators. To find the amount of, let's say, 20 dollar notes, we do an integer division (//) of the amount left to pay. We then change the value of left_to_pay to the remainder of the division (calculated with the modulo operator %.)

As you can see, this gets repetitive really fast. This is ripe for typos and bugs, so at the end of the post, you'll find a bonus version that will introduce ways to make our code tighter.

As an exercise, adapt the program to euro bills and coins.

Recap of new concepts

Arithmetic operators

Our program used most arithmetic operators: +, -, *, /, //, %. We didn’t use ** (exponent).

Python follows PEMDAS:

  1. parentheses

  2. exponents

  3. multiplications, division, floor division (//), modulo (%)

  4. addition, subtraction

Program design

We used an iterative process for our program.

First, I wrote a general structure outline, dividing it into sections (input, calculations, and output). At that point, the code didn’t serve any purpose yet, but it was a canvas we could use to guide us.

We then evolved the program through small, manageable steps. A cool effect of this iterative process is that testing and detecting bugs is more manageable.

We divided our program using functional and logical groupings: input, calculations, and output. When I added new functionality, each part went into its place smoothly. It’s also easier to read. Imagine if we mixed everything. Your mind would have to switch from input to calculations, to output, back to calculations, then to inputs, etc.

Escape sequences

Escape sequences are special character sequences you can use in strings. They start with a backslash (\). This lets you use tabs (\t), newlines (\n), backslashes (\\), single quotes (\'), and double quotes (\"). There exist other escape sequences, but sincerely speaking, I have never seen them used in modern code (vertical tabs, form feeds, etc.)

A bit of practical advice: keep escape sequences to a minimum to keep output strings legible. Using triple-quoted strings can simplify code and make it more readable.

Format specifiers

You format numerical values using a format specifier, controlling the number of decimal places, alignment, padding, usage of scientific notation, etc. A colon introduces format specifiers. It’s good practice to format your numbers to avoid unnecessary or useless digits. For example, 15.71000000001 can very well become 15.71, or a price only needs to be displayed with two decimals (cents). Try to keep your formatting consistent across your program.

Exercises

1.

Extend the tip calculator to validate that the meal cost is positive and that the tip percentage is between 0 and 100.

Display appropriate error messages for invalid inputs.

2.

Modify the calculator to add a fixed service charge (e.g., $2.50)

3.

Experiment with escape sequences to add a new line before the summary, a tab before each line in the summary, and display the discount percentage as a percentage.

4.

Calculate a tax percentage (e.g., 7.5%) on the discounted price. This calculation should be different from the tip calculation.

5.

Extend the bill-splitting feature so one person can pay a different share than the others (e.g., 60-40 split, 20-40-40 split, etc.)

6.

Explore format modifiers:

  • display amounts with 4 decimal places.

  • align values to the right of a 10-character wide field

  • use scientific notation

Bonus Version: a taste of future topics

OK, this is the last version of our program. It is here to whet your appetite, and you’ll get a glimpse of two core concepts: loops and lists. Don’t worry if some of it makes no sense because those concepts will be explained properly in future posts.

def main():
    #############################################
    # Get input
    #############################################

    # Get price
    price = input("How much does your meal cost? ").strip()
    if price:
        price = float(price)
    else:
        print("Error: the price is not right.")
        return

    # Get tipping percentage
    tip_percent = input("How much do you want to tip in percents? ").strip()
    if tip_percent:
        tip_percent = int(tip_percent)
    else:
        print("Error: tip_percent is not right")
        return

    # Get discount percentage
    # Discount gets applied to base price and doesn't affect tipping
    discount_percent = input("Discount percentage from base price (if any, else 0): ").strip()
    if discount_percent:
        discount_percent = float(discount_percent)
    else:
        discount_percent = 0

    # Get bill splitting info
    number_of_splitters = 1
    splitting_p = input("Will you split the bill? (yes or no): ").strip().lower()
    if splitting_p and (splitting_p == 'yes' or splitting_p == 'y'):
        number_of_splitters = input("How many people to split the bill? ").strip()
        number_of_splitters = int(number_of_splitters)

    #############################################
    # Calculations
    #############################################

    # Calculate tip
    tip = price * tip_percent / 100
    price_with_tip = price + tip

    # Calculate discount on price with tip_percent
    discount_amount = price * discount_percent / 100
    discounted_price = price_with_tip - discount_amount

    # Split the bill
    amount_per_person = discounted_price / number_of_splitters

    # Pay with cash
    left_to_pay = amount_per_person
    values =  [100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01]
    bills_and_coins = []
    for i in range(0, len(values)):
        bills_and_coins.append(int(left_to_pay // values[i]))
        left_to_pay = left_to_pay % values[i]


    #############################################
    # Display results
    #############################################

    print(f"""
    Summary:
        Base price:           {price}
        Tip percentage:       {tip_percent}
        Discount percentage:  {discount_percent}
        Tip amount:           {tip}
        Price with tip:       {price_with_tip}
        Discount amount:      {discount_amount}
        Discounted price:     {discounted_price}
        Amount per person:    {amount_per_person:.2f}

    Pay with cash: {bills_and_coins[0]} hundred $ bills
                   {bills_and_coins[1]} fifty $ bills
                   {bills_and_coins[2]} twenty $ bills
                   {bills_and_coins[3]} ten $ bills
                   {bills_and_coins[4]} five $ bills
                   {bills_and_coins[5]} one $ bills
                   {bills_and_coins[6]} quarters
                   {bills_and_coins[7]} dimes
                   {bills_and_coins[8]} nickels
                   {bills_and_coins[9]} singles
    """)

if __name__ == '__main__':
    main()

This version introduces important programming concepts like loops, lists, and indexing. We’ll cover them later, but here’s a short primer.

Lists

Lists are a basic Python data structure. Our program contains two lists: values and bills_and_coins.

Lists are collections that can contain data. This data can be integers, floats, strings, or other lists. You define lists by using square brackets like this:

values =  [100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01]
bills_and_coins = []

The lists we used contained data of the same type, but this is not obligatory:

var_1 = 1 / 4       # a variable
def function_1(a):  # a function
    return a + 1
my_list = [1, “a string”, 3.14, var_1, function_1]

Lists are ordered. The list’s elements have the same place during the list’s lifetime. You can access them using their position (their index).

append is one of the ways you can manipulate lists. It adds an element to the end of the list. We’ll see later why we write my_list.append(my_element) and not my_list = my_list.append(my_element). (i.e., we don’t use assignment to modify lists with append)

Indexing

You can access a list’s elements using their index. The index of an element is its position in the list. Indexes start at 0. You access an element like this:

list[index]

Or in our program:

values[i]          # the element at the i-th position 
bills_and_coins[0] # the first element (at the zeroth position)
bills_and_coins[1] # the second element (position 1)
bills_and_coins[2] # the third element (position 2)

Loops

Loops are ways your program can do a task several times until a condition is met.

Python has two looping mechanisms, conveniently named while and for.

Our program contains a for loop:

for i in range(0, len(values)):
        bills_and_coins.append(int(left_to_pay // values[i]))
        left_to_pay = left_to_pay % values[i]

The first line tells Python to go through all of value’s indexes in order, while the two following lines tell it what to do for each index.

You can use other names instead of i; it’s just that i is the generic name for an index.

Here’s how you can see each index that gets generated through a for loop:

>>> for i in range(0, 10):
...     print(i)
... 
0
1
2
3
4
5
6
7
8
9

In our example, i takes each integer starting from the range’s first argument up to but not including the second argument. This last part can cause mistakes because it’s easy to forget the last argument is omitted.

Here’s how you can think of the above example’s execution:

stepvalue of i
set i = 00
check if it’s equal to 100
it’s not0
print(i)0
add 1 to i1
check if it’s equal to 101
it’s not1
print(i)1
add 1 to i2
check if it’s equal to 102
it’s not2
print(i)2
some steps omitted…some values omitted…
add 1 to i10
check if it’s equal to 1010
IT IS!!!10
EXIT THE LOOPi gets discarded

In real life, Python is more complex than this, but this is a simple and sufficient mental model to think of for loops.

The name that you use to iterate can be reused in the loop. For example, our program used i to access specific elements in the list values with values[i].

We could have made many more adjustments to our program, but we would have strayed too far off-topic. Tell us below what you think!

« Previous article

Next article »

0
Subscribe to my newsletter

Read articles from Had Will directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Had Will
Had Will