PythonMastery
beginner 20 min read · lesson 1 of 15 in Projects

Project: Tip Calculator

1 · The lesson

read

You're going to build a small, working program that asks for a bill amount, asks how many people are splitting, and prints exactly what each person owes — tip included. It's the kind of program a non-programmer would actually use.

What you'll practice: input(), type conversion, arithmetic, f-strings, exception handling, functions.

What you'll have at the end: a Python script you can run on your phone or laptop to settle the next restaurant bill.


Step 1 — The Simplest Possible Version

Start with hardcoded values. Get the math right first.

python
bill = 87.50
tip_percent = 18
num_people = 3

tip = bill * tip_percent / 100
total = bill + tip
per_person = total / num_people

print(f"Tip ({tip_percent}%):   ${tip:.2f}")
print(f"Total:           ${total:.2f}")
print(f"Per person:      ${per_person:.2f}")

Run it. Tweak the values and run again. Make sure the numbers look right.


Step 2 — Wire Up input()

Now make it interactive. Remember: input() returns a string, so we need to convert.

python
# In a real terminal, you'd use input(). In the browser, we simulate it:
bill_text = "87.50"        # input("How much was the bill? $")
tip_text = "18"            # input("Tip percent? ")
people_text = "3"          # input("How many people? ")

bill = float(bill_text)
tip_percent = float(tip_text)
num_people = int(people_text)

tip = bill * tip_percent / 100
total = bill + tip
per_person = total / num_people

print(f"Tip ({tip_percent:.0f}%):   ${tip:.2f}")
print(f"Total:           ${total:.2f}")
print(f"Per person:      ${per_person:.2f}")

In a real script, just swap the comments back in:

python
# bill = float(input("How much was the bill? $"))
# tip_percent = float(input("Tip percent? "))
# num_people = int(input("How many people? "))

Step 3 — Handle Bad Input Gracefully

What if someone types "abc" or "-50"? The naive version crashes. The real version explains.

python
def get_positive_number(prompt, source_text, allow_int_only=False):
    """Convert a string to a positive number, raising a friendly error otherwise."""
    try:
        value = int(source_text) if allow_int_only else float(source_text)
    except ValueError:
        raise ValueError(f"{prompt}: '{source_text}' isn't a valid number")
    if value <= 0:
        raise ValueError(f"{prompt}: must be positive, got {value}")
    return value

try:
    bill = get_positive_number("Bill amount", "87.50")
    tip_percent = get_positive_number("Tip percent", "18")
    num_people = get_positive_number("Number of people", "3", allow_int_only=True)

    tip = bill * tip_percent / 100
    total = bill + tip
    per_person = total / num_people

    print(f"Tip ({tip_percent:.0f}%):   ${tip:.2f}")
    print(f"Total:           ${total:.2f}")
    print(f"Per person:      ${per_person:.2f}")
except ValueError as e:
    print(f"⚠️  {e}")

The pattern: validate inputs in a helper function, raise descriptive ValueErrors, catch them all in one place. Notice how the math hasn't changed — it's just wrapped in trust.

Try changing "87.50" to "abc" or "3" to "0" and click Run again. You'll see the friendly error message instead of a traceback.


Step 4 — Make It a Reusable Function

Wrap the calculation so other code can call it.

python
def calculate_split(bill, tip_percent, num_people):
    """Return a dict with tip, total, and per-person amounts."""
    if bill <= 0 or tip_percent < 0 or num_people <= 0:
        raise ValueError("All inputs must be positive (tip can be zero)")
    tip = bill * tip_percent / 100
    total = bill + tip
    return {
        "tip": tip,
        "total": total,
        "per_person": total / num_people,
    }

# Try it with a few scenarios
scenarios = [
    (87.50, 18, 3),
    (45.00, 20, 2),
    (120.00, 15, 4),
]

for bill, tip_pct, people in scenarios:
    result = calculate_split(bill, tip_pct, people)
    print(f"${bill:>7.2f} @ {tip_pct}% for {people} people → ${result['per_person']:.2f} each")

Now the same logic powers a single-bill calculator AND a comparison table — without duplicating any math.


Stretch Goals — Make It Yours

Pick any of these to extend the project:

1. Round up per-person amount to the next 50¢ so the math is friendlier in real life. Hint: math.ceil(amount * 2) / 2.
2. Add a "service quality" prompt that maps "great"/"good"/"ok" to 20%/18%/15% automatically.
3. Subtotal by item — let the user enter several item prices and sum them.
4. Track the running tally — wrap it in a while True: loop that lets the user calculate multiple bills until they type quit.
5. Save the history — append each calculation to a bills.txt log with a timestamp (you'll use the File I/O lesson for this).


🎯 Your Turn — Build It Without Peeking

Now write your own version from scratch. The skeleton below has TODOs. Try to fill them in before opening the hints.

Goal: a function split_bill(bill, tip_percent, num_people) that returns the per-person amount including tip. Validates that all inputs are positive.

python
def split_bill(bill, tip_percent, num_people):
    # TODO 1: validate all three are positive — raise ValueError if not
    # TODO 2: calculate the tip
    # TODO 3: return (bill + tip) / num_people
    pass

# In a real terminal, get values from input() and pass them in.
# For testing here, hardcode some numbers:
result = split_bill(87.50, 18, 3)
print(f"Per person: ${result:.2f}")  # expect ≈ $34.42
Hint 1 — Validation pattern Use a single if with or to check all three at once: if bill <= 0 or tip_percent < 0 or num_people <= 0: raise ValueError("...") Tip percent can be zero (no tip), but bill and num_people must be positive.
Hint 2 — The math tip = bill * tip_percent / 100, then return (bill + tip) / num_people. That's it.
Show full solution
python
def split_bill(bill, tip_percent, num_people):
    if bill <= 0 or tip_percent < 0 or num_people <= 0:
        raise ValueError("bill and num_people must be positive; tip can be zero")
    tip = bill * tip_percent / 100
    return (bill + tip) / num_people

# With real input():
# bill = float(input("Bill amount: "))
# tip_percent = float(input("Tip %: "))
# num_people = int(input("Number of people: "))
# print(f"Each pays: ${split_bill(bill, tip_percent, num_people):.2f}")

print(f"Per person: ${split_bill(87.50, 18, 3):.2f}")

What You Built

Look at the final code. You now have:


  • Input validation that doesn't crash

  • A reusable function with a clear contract

  • Money formatting with :.2f

  • An exception-handling pattern you'll reuse forever

This is what a small Python program actually looks like in real code. Not toy x = 5 exercises — a tiny script that does one useful thing well.

Next project: Number Guessing Game — adds loops and random numbers.

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code actually did, not just whether a test passed.