10  Exceptions, Errors, and Debugging

In This Chapter

When you write a game, you know exactly how it is supposed to be played. You know that if the screen says “Enter your age,” you should type 15. But users are always unpredictable. A player might type fifteen, or apple, or just mash ESC on the keyboard.

If we don’t plan for that, our program can instantly crash. In this chapter, we are going to learn how to protect our code from unpredictable users, and how to track down the bugs that we accidentally cause ourselves. We will cover:

  • Exceptions: What happens when Python panics.
  • The Safety Net: Using try and except to catch crashes.
  • Specific Errors: Why catching everything is a terrible habit.
  • The pass Keyword: How to silently ignore problems.
  • The Ultimate Input Trap: Forcing users to behave.
  • The Art of Debugging: How to read Tracebacks and fix broken code.
  • The Project: The Uncrashable Calculator.

10.1 The Concept of Crashing (Exceptions)

When Python encounters an error it doesn’t know how to handle, it stops the program entirely. We sometimes say that it crashed, but in Python it has an official name: an Exception.

Let’s look at a classic crash. If we ask a user for a number, they might type a word.

age_text = input("How old are you? ")
# If the user types "sixteen", the next line will crash the program!
age = int(age_text) 
print(f"You will be {age + 1} next year.")

You you run this and enter 16 it will print You will be 17 next year. But if you enter sixteen, this is the output:

ValueError: invalid literal for int() with base 10: 'sixteen'

Python attempts to cast the letters S-I-X-T-E-E-N into an integer. It realizes this is mathematically impossible and throws a ValueError. The program dies instantly, and the print statement is never reached.

10.2 The Safety Net: try and except

We cannot stop users from typing the wrong thing, but we can stop Python from panicking. We do this using a try / except block.

It works exactly how it sounds: we tell Python to try running a block of code. If it works, great! If it crashes, Python will immediately jump down to the except block instead of killing the program.

age_text = input("How old are you? ")

try:
    # Python tries to run this risky code...
    age = int(age_text)
    print(f"You will be {age + 1} next year.")
except:
    # If the risky code blows up, it jumps here!
    print("ERROR: You must type a number, not a word!")

print("The program continues safely.")

Now, if the user types "sixteen", the program doesn’t crash or panic. It skips the rest of the try block, prints ERROR: You must type a number, not a word!, and peacefully continues to the bottom of the script. We have successfully caught the exception!

Catching Specific Errors (No “Naked” Excepts)

In the example above, we wrote except:. This is called a “naked except,” and it is actually a very bad habit.

A naked except catches every possible error in the universe. What if you accidentally spelled print wrong inside the try block? The except block would catch that typo, print the warning about typing a number, and hide your typo from you forever!

Instead you should always specify exactly which error you are trying to catch:

try:
    age = int(age_text)
except ValueError:
    print("ERROR: You must type a number, not a word!")

Now, this block will only catch a ValueError (which happens during a failed cast). If a different error occurs, the program will crash normally, allowing you to see what really went wrong.

Catching Multiple Errors

Some code is dangerous in multiple ways. For example, in math, dividing by zero is impossible. If you try to do 10 / 0, Python will crash with a ZeroDivisionError.

You can stack except blocks just like elif blocks to handle different threats! Look at this example that lets you enter a number of people and a number of pizza slices to calculate how many slices each person gets.

try:
    slices = int(input("How many slices? "))
    people = int(input("How many people are eating? "))
    slices_per_person = slices / people
    print(f"Everyone gets {slices_per_person} slices.")
    
except ValueError:
    print("You didn't type a number!")
except ZeroDivisionError:
    print("You can't divide pizza among zero people!")

If the user enters something that is not a number it will tell them what they did wrong instead of just crashing. If the user tries to divide by 0 they will get a different message.

The pass Keyword

Sometimes, you don’t want to print an error message. You just want the computer to silently ignore the crash and move on. You can use the pass keyword inside an except block to tell Python to literally “do nothing.”

try:
    age = int(input("Age: "))
except ValueError:
    pass # Silently ignores the error.

10.3 The Ultimate Input Trap

When we first learned about loops, we built an “Input Trap” using a while True loop and .isdigit(). It worked, but it wasn’t perfect. For example, .isdigit() doesn’t understand negative numbers (like -5) because it thinks the minus sign is a letter!

Now that we have try / except, we can build a better input trap:

while True:
    try:
        user_input = int(input("Enter a number: "))
        # If the line above succeeds, we break the loop!
        break 
    except ValueError:
        print("Invalid input. Please try again.")

print(f"You successfully chose {user_input}.")

This loop will continue to trap the user until they provide a valid integer, handling negatives, words, and empty spaces perfectly.

10.4 The Art of Debugging

Errors caused by users are annoying, but errors caused by you are part of the job. Writing code without bugs is impossible. Finding and fixing those bugs is a skill called Debugging.

1. Reading the Traceback

When your program crashes, Python prints a block of angry red text called a Traceback. You should learn to read the traceback carefully as it give you many clues to what is going wrong.

Always read a Traceback from the bottom up:

  1. The Bottom Line: Tells you exactly what went wrong (NameError, TypeError, SyntaxError).
  2. The Lines Above It: Tell you exactly where it went wrong (the file name and the exact line number).
Traceback (most recent call last):
  File "my_game.py", line 45, in <module>
    calculate_damage(player_sword)
NameError: name 'player_sword' is not defined

This Traceback tells you everything: Go to line 45. You tried to use a variable called player_sword, but you forgot to create it!

3. Rubber Duck Debugging

This will sound silly, but your ability to do this will make you a much better programmer.

When you stare at broken code for too long, your brain tricks you into seeing what you meant to write, instead of what you actually wrote. To break this illusion, place a rubber duck (or a toy, or a stuffy, or a pet) on your desk. Explain your code to the duck, line by line, out loud. You need to explain the logic of what is happening from beginning to end.

“Okay duck, first I set health to 100. Then I check if the sword is true. Then I… oh wait. I accidentally typed one equal sign instead of two. I’m assigning the sword instead of checking it!”

By forcing yourself to explain the logic out loud, you will often catch your own mistakes. This is a good excercise to do with other people’s code as well. When you get to long and complicated project code examples in this course try to describe how they work outloud line by line.

10.5 The Project: The Uncrashable Calculator

We are going to build a text-based calculator. But instead of just doing math, we are going to challenge the user to try and break it.

We will use a while True loop to keep the calculator running, and a try/except block to catch ValueError (typing words) and ZeroDivisionError (dividing by zero).

Type this code into a new file, save it as uncrashable.py, and run it. Try your hardest to make it crash!

print("--- THE UNCRASHABLE CALCULATOR ---")
print("I will divide two numbers for you. Try to break me!")

while True:
    print("\n-------------------------")
    num1_text = input("Enter the first number (or type 'quit'): ")
    
    # Give the player a way to escape the loop!
    if num1_text.lower() == 'quit':
        print("Goodbye!")
        break
        
    num2_text = input("Enter the second number: ")
    
    try:
        # Step 1: Try to cast the inputs to floats (so decimals work too!)
        number1 = float(num1_text)
        number2 = float(num2_text)
        
        # Step 2: Try to do the division
        answer = number1 / number2
        
        # Step 3: Print the success!
        print(f"The answer is: {answer}")
        
    except ValueError:
        print("NICE TRY! You have to type actual numbers, not words!")
        
    except ZeroDivisionError:
        print("NICE TRY! The universe will explode if you divide by zero!")

Challenges

Test your new bulletproofing skills with these challenges.

Challenge 1: The Silent Trap

You wrote a game that asks the user to pick a character class (1 for Warrior, 2 for Mage). Open a new file and write an Input Trap that asks: "Enter 1 for Warrior or 2 for Mage: ".

Use a try/except block. If they type a word instead of a number, use the pass keyword to silently ignore them and just ask the question again until they type an integer!

Challenge 2: The Bug Hunt

The following code has three major bugs in it. A SyntaxError, a NameError, and a TypeError. Read the code, find the bugs, and fix them so the script runs perfectly.

# Broken Code!
def calculate_score(points, bonus)
    total = points + bonus
    return totl

score_text = "50"
final_score = calculate_score(score_text, 10)

print(f"Your final score is {final_score}")

(Hint: Remember what happens when you try to do math with a string and an integer!)