Python Classes: Shopping Cart and Calculator – PY101, Worksheet 2 – Study Notes
offline

Difficulty: Beginner–Intermediate | Prerequisites: Basic Python classes (see Part 1: Person and Bank)

TL;DR

Challenge 3 builds a ShoppingCart class that stores items in a dictionary, lets you add and remove them, and totals up the price. Challenge 4 builds a Calculator class with methods for add, subtract, multiply and divide, plus error handling for division by zero. Together they reinforce how to use dictionaries as instance attributes, write methods that modify shared state, and handle exceptions with try/except.


Key Terms

Dictionary (dict)

A built-in Python data structure that maps keys to values: {"Papaya": (100, 1.50)}. In these challenges, dictionaries store item names mapped to quantity-price tuples (Shopping Cart) or are not used directly (Calculator). Think of it as a lookup table where you find a value by its label.

Tuple

An immutable, ordered collection written with parentheses: (quantity, price). The Shopping Cart worksheet stores each item's quantity and price as a tuple inside the dictionary. In simple terms, a tuple is a fixed-size container whose contents cannot be changed after creation.

try / except

Python's mechanism for catching runtime errors. Code that might fail goes inside try; if it raises an exception, execution jumps to the matching except block instead of crashing the program. Think of it as a safety net that lets you handle errors gracefully.

ZeroDivisionError

The specific exception Python raises when you divide by zero. The Calculator challenge catches this with except ZeroDivisionError: to print a friendly message instead of a traceback.

ValueError

Raised when a function receives an argument of the right type but an inappropriate value, for example int("abc"). The Calculator's main() loop catches this to handle non-numeric input.

del

A statement that removes a variable or a dictionary entry: del self.items[name]. After del, that key no longer exists in the dictionary.

input()

A built-in function that pauses the program, prints a prompt, and returns whatever the user types as a string. Both the Shopping Cart and Calculator solutions use input() for interactive console sessions.


Core Content: Challenge 3 – the Shopping Cart Class

What the challenge asks

Create a class with three methods: add_item, remove_item and calculate_total. Items live in a dictionary. Create an object, add some items, remove one, and print the total before and after.

Data structure

The worksheet stores items as a dictionary where each key is the item name and each value is a tuple (quantity, price_per_unit):

self.items = {"Papaya": (1, 100), "Quava": (1, 200), "Orange": (1, 150)}

This makes it straightforward to look up an item by name, update its quantity, or delete it entirely.

Method breakdown

  • add_item: prompts for a name, quantity and unit price. If the item already exists, it updates the quantity (adds to the current count) while keeping the price. If it is new, it inserts a fresh entry.

  • remove_item: prompts for an item name and a quantity to remove. If the quantity to remove is greater than or equal to what is in the cart, the item is deleted with del. Otherwise, the quantity is reduced and the entry is updated.

  • calculate_total: loops through self.items.items(), unpacking each value into (quantity, price), and accumulates quantity * price into a running total. Returns and prints the result.

Reference implementation

class ShoppingCart:
    def __init__(self, items=None):
        if items is None:
            items = {}
        self.items = items

    def add_item(self):
        name = input("Enter name of item: ")
        num = int(input(f"Enter the quantity of {name}: "))
        price = float(input(f"Enter the price per unit of {name}: "))
        if name in self.items:
            current_quantity, current_price = self.items[name]
            self.items[name] = (current_quantity + num, price)
        else:
            self.items[name] = (num, price)
        print(f"Updated shopping list: {self.items}")

    def remove_item(self):
        name = input("Enter name of item you want to remove: ")
        if name in self.items:
            num = int(input(f"Enter the number of {name} to remove: "))
            current_quantity, price = self.items[name]
            if num >= current_quantity:
                del self.items[name]
                print(f"{name} has been completely removed.")
            else:
                self.items[name] = (current_quantity - num, price)
                print(f"Updated quantity of {name}: {self.items[name][0]}")
        else:
            print(f"{name} is not in the shopping list.")

    def calculate_total(self):
        total_cost = 0
        for item, (quantity, price) in self.items.items():
            total_cost += quantity * price
        total_cost = round(total_cost, 2)
        print(f"Total cost of all items: £{total_cost}")
        return total_cost

cart = ShoppingCart()
cart.add_item()   # e.g. Papaya, 1, 100
cart.add_item()   # e.g. Quava, 1, 200
cart.add_item()   # e.g. Orange, 1, 150
cart.calculate_total()
cart.remove_item() # e.g. Orange
cart.calculate_total()

Expected output

Current items in cart:
Papaya = 100
Quava = 200
Orange = 150
Total Quantity: 450

Updated items in Cart after removing orange:
Papaya = 100
Quava = 200
Total Quantity: 300

Why items=None instead of items={}

Using a mutable default argument like items={} is a classic Python trap. Because default arguments are evaluated once when the function is defined, every instance created without passing items would share the same dictionary object. The None guard (if items is None: items = {}) gives each instance its own fresh dictionary.

Real-world application

This is a simplified version of any e-commerce cart. The pattern of storing items in a dictionary, looking them up by key, and iterating to compute a total is how real cart systems work before the data is persisted to a database.


Core Content: Challenge 4 – the Calculator Class

What the challenge asks

Create a Calculator class with methods for adding, subtracting, multiplying and dividing. Create an object, call the methods, and include error handling for division by zero.

Two design approaches in the worksheet

The worksheet shows two styles side by side:

  • Class-based (matches the brief): a Calculator class with add, subtract, multiply and divide methods, each taking two numbers and returning the result.

  • Function-based (the screenshot solution): standalone functions (add(), sub(), mult(), divi()) called from a main() loop with a menu. This works but does not use a class, so it only partially answers the challenge.

For exam purposes, know the class-based version.

Class-based reference implementation

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

    def divide(self, a, b):
        if b == 0:
            return "Cannot divide by zero"
        return a / b

calc = Calculator()
print(f"7 + 5 = {calc.add(7, 5)}")
print(f"34 - 21 = {calc.subtract(34, 21)}")
print(f"54 * 2 = {calc.multiply(54, 2)}")
print(f"144 / 2 = {calc.divide(144, 2)}")
print(f"45 / 0 = {calc.divide(45, 0)}")

Expected output

7 + 5 = 12
34 - 21 = 13
54 * 2 = 108
144 / 2 = 72.0
45 / 0 = Cannot divide by zero

Error handling with try / except

The worksheet's function-based solution wraps the division call in a try block:

try:
    total = divi()
except ZeroDivisionError:
    print("Number is not divisible by zero")

Key points:

  • try runs the risky code. If no exception occurs, except is skipped entirely.

  • except ZeroDivisionError catches only that specific error. Other exceptions (like TypeError) would still crash the program.

  • except ValueError is used separately to catch non-numeric input from int(input(...)).

  • You can have multiple except blocks under one try.

The alternative approach (used in the class-based version above) is to check if b == 0 before dividing. Both are valid; the try/except approach is more Pythonic when the error is genuinely exceptional rather than a routine check.

The main() loop pattern

The worksheet's screenshot uses a while loop with a menu:

def main():
    x = True
    while x:
        choice = int(input("Choose 1-5: "))
        if choice == 1:
            total = add()
        elif choice == 5:
            print("Exiting the program")
            x = False
        else:
            print("Invalid input")

This is a common pattern for console applications: loop until the user chooses to exit, using a flag variable (x) to control the loop.

Real-world application

The Calculator class is deliberately simple, but the pattern of exposing operations as methods on an object is how libraries and APIs are designed. A database client, for example, exposes .query(), .insert(), .delete() in much the same way. The error-handling pattern (catching ZeroDivisionError, ValueError) is essential in any program that accepts user input.


Common Misconceptions

  • Using items={} as a default argument. This is the single most common beginner mistake in Python. Mutable defaults are shared across all calls. Use items=None and create a new dictionary inside the method.

  • Forgetting to update self after a calculation. Printing a deposit or withdrawal amount does nothing to the object's state unless you also write self.deposit_amount += amount (or the equivalent). The same applies to removing items from a cart: you need del self.items[name] or an updated assignment.

  • Catching bare except: without naming the exception. A bare except catches everything, including KeyboardInterrupt and SystemExit, which makes the program hard to stop. Always name the exception you expect: except ZeroDivisionError, except ValueError.

  • Thinking try/except replaces validation. try/except is for genuinely unexpected errors. If you can check a condition first (like if b == 0), that is often clearer. Both approaches are valid, but mixing them inconsistently in the same codebase is confusing.

  • Returning inside a try block without realising the except also needs a return. If your try returns a value on success but your except only prints, the caller gets None on the error path.


Why It Matters / Exam Flags

  • ⚠️ Dictionary operations (lookup, insertion, deletion, iteration with .items()) are heavily tested. Know the difference between dict[key] (raises KeyError if missing) and dict.get(key, default).

  • ⚠️ try/except is a near-certainty on the exam. Be ready to write a try block that catches a named exception and explain what happens if the exception is not caught.

  • ⚠️ The mutable default argument trap (items={}) is a favourite exam trick question. Know the fix.

  • ⚠️ Expect a question asking you to trace through a Shopping Cart or Calculator scenario: "After adding X and removing Y, what does the dictionary contain?"


Quick Self-Test

  1. True or false: items={} as a default argument creates a new dictionary for every new object.

  1. What does del self.items["Orange"] do?

  1. Fill in the blank: To catch a division-by-zero error, you write except ______.

  1. True or false: If no exception occurs, the except block is still executed.

  1. What value does a function return if it reaches the end without a return statement?

Answers: 1. False (all objects share the same dictionary; use None instead). 2. It removes the key "Orange" and its value from the dictionary. 3. ZeroDivisionError. 4. False (it is skipped entirely). 5. None.


Practice Q&A

Q: A ShoppingCart is created with cart = ShoppingCart(). You add Papaya (qty 1, price 100) and Quava (qty 1, price 200). What does cart.calculate_total() return?

A: 300. The method computes (1 * 100) + (1 * 200) = 300.

Q: After the total above, you call cart.remove_item() and remove Papaya entirely. What does cart.items look like now?

A: {"Quava": (1, 200)}. The Papaya entry has been deleted.

Q: Write a try/except block that attempts to divide a by b and prints "Cannot divide by zero" if b is zero.

A:

try:
    result = a / b
except ZeroDivisionError:
    print("Cannot divide by zero")

Q: Why is except Exception broader than except ZeroDivisionError?

A: Exception is the base class for most built-in exceptions. Catching it will also catch ValueError, TypeError, KeyError and many others, not just division by zero. Catching a specific exception is safer because it lets unexpected errors surface.

Q: In the Calculator's main() function, what is the purpose of the variable x = True and the line x = False?

A: x is a flag that controls the while loop. Setting it to False causes the loop condition while x == True to fail on the next iteration, which exits the menu.


Connections to Other Topics

The dictionary-as-state pattern from the Shopping Cart leads directly into file I/O topics where you read items from a CSV and load them into a dictionary, or write the cart's contents back to a file. It also connects to JSON serialisation, since a dictionary maps naturally to a JSON object.

The try/except pattern from the Calculator is the foundation of robust file handling (except FileNotFoundError), network requests (except ConnectionError), and any code that interacts with the outside world. Later units on testing will revisit exceptions from the other direction, using pytest.raises to verify that code throws the right error.


Related Terms / Search Tags

Python class, ShoppingCart, shopping cart class Python, Calculator class Python, dictionary, dict, tuple, add_item, remove_item, calculate_total, try, except, ZeroDivisionError, ValueError, del, input(), mutable default argument, error handling Python, exception handling, while loop menu, PY101, Foundations of Computer Science