Errors and Exceptions

What Are Errors?

Errors (also called exceptions) are problems that occur when your program runs. When Python encounters an error, it stops running and displays an error message.

Example:

age = int(input("Enter your age: "))
# If the user types "twenty" instead of "20", Python crashes with an error!

Output:

Enter your age: twenty
ValueError: invalid literal for int() with base 10: 'twenty'

Without error handling, your program crashes and stops. With error handling, you can catch these errors and keep your program running.


Handling errors with try and except

The try/except pattern lets you catch errors and handle them gracefully instead of crashing. There are parallels with the if/elif/else control flow patterns.

Basic Syntax

try:
    # Code that might cause an error
    risky_code()
except:
    # What to do if ANY error occurs
    print("Something went wrong!")

How it works:

  1. Python tries to run the code in the try block
  2. If an error occurs, it jumps to the except block
  3. If no error occurs, the except block is skipped

Simple Example

# Without error handling - crashes!
age = int(input("Enter your age: "))
print(f"You are {age} years old")

Without error handling, if the user enters an invalid response, e.g. a string that can not be converted to an int, this throws an error and the whole program crashes and stops:

Enter your age: ten
...
ValueError: invalid literal for int() with base 10: 'ten'

With the try/except pattern, you can instead handle the error, e.g. by printing out a message and continuing.

# With error handling - doesn't crash!
age = int(input("Enter your age: "))
print(f"You are {age} years old")

# python first tries the code in the try block
try:
    age = int(input("Enter your age: "))
    print(f"You are {age} years old")
except:
    print("That's not a valid age!")

This gives the output:

Enter your age: ten
That's not a valid age!
# Program continues running!

Common Types of Errors

Some common errors that are encountered with even simple Python code include:

  • ValueError - Invalid conversions
  • TypeError - Wrong data types
  • KeyError - Missing dictionary keys
  • IndexError - Invalid list indices
  • FileNotFoundError - Missing files
  • ZeroDivisionError - Division by zero

ValueError

Happens when you try to convert something to the wrong type.

age = int("twenty")  # ValueError: can't convert "twenty" to integer
number = float("abc")  # ValueError: can't convert "abc" to float

TypeError

Happens when you use the wrong type of data for an operation.

result = "5" + 5  # TypeError: can't add string and integer
name = "Alice"
name[0] = "B"     # TypeError: strings are immutable

KeyError

Happens when you try to access a dictionary key that doesn’t exist.

flashcards = {"Dog": "Hund", "Cat": "Katze"}
translation = flashcards["Bird"]  # KeyError: 'Bird' doesn't exist

IndexError

Happens when you try to access a list index that doesn’t exist.

scores = [85, 92, 78]
score = scores[5]  # IndexError: list only has 3 items (indices 0-2)

FileNotFoundError

Happens when you try to open a file that doesn’t exist.

with open("missing_file.txt", "r") as file:
    content = file.read()  # FileNotFoundError: file doesn't exist

ZeroDivisionError

Happens when you try to divide by zero.

result = 10 / 0  # ZeroDivisionError: division by zero

Catching Specific Errors

It’s better to catch specific error types so you know exactly what went wrong.

Single Exception Type

try:
    age = int(input("Enter your age: "))
    print(f"You are {age} years old")
except ValueError:
    print("Please enter a number, not text!")

Multiple Exception Types

try:
    flashcards = {"Dog": "Hund", "Cat": "Katze"}
    word = input("Enter a word: ")
    translation = flashcards[word]
    print(f"{word} = {translation}")
except KeyError:
    print("That word isn't in the flashcards!")
except ValueError:
    print("Invalid input format!")

Catching Multiple Errors with One Block

try:
    result = 10 / int(input("Enter a number: "))
    print(result)
except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero!")

The else and finally clauses

In addition to try and except, else and finally are two additional keywords that can be used to further control error handling behaviour.

else - Runs if NO error occurs

try:
    age = int(input("Enter your age: "))
except ValueError:
    print("Invalid age!")
else:
    # Only runs if no error occurred
    print(f"You are {age} years old")

finally - Always runs (regardless of whether there is an error or not)

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found!")
finally:
    # Always runs, even if there was an error
    print("Closing file operations")
    file.close()

Note: When using with statements, you don’t need finally for files because they close automatically.

Complete pattern

try:
    # Try to do something risky
    result = risky_operation()
except SpecificError:
    # Handle specific error
    print("Specific error occurred")
else:
    # Only if no error
    print("Success!")
finally:
    # Always runs
    print("Cleanup complete")

Good practices


Good Practices

Catch specific errors - not all errors blindly

Specific error handling helps you understand precisely what is wrong since you can specify different behaviours/messages for different kinds of errors. You can also use it to identify errors that were not expected by forcing the program to fail or print out a special message indicating that the error was not anticipated.

❌ Bad practice - catching everything:

try:
    age = int(input("Enter age: "))
    score = scores[age]
except:
    print("Something went wrong!")
    # What went wrong? ValueError? IndexError? We don't know!

✅ Good practice - catching specific errors:

try:
    age = int(input("Enter age: "))
    score = scores[age]
except ValueError:
    print("Please enter a valid number!")
except IndexError:
    print("Age is out of range for the scores list!")

Provide helpful messages - tell users what went wrong

Error messages should be an intrinsic part of your program’s design since it can have a significant impact on its user-friendliness. Helpful error messages should help users to understand and correct what they did wrong so they can interact effectively with your program.

❌ Bad practice - catching everything:

try:
    flashcards = {"Dog": "Hund", "Cat": "Katze"}
    word = input("Enter a word: ")
    print(flashcards[word])
except KeyError:
    print("Error!")  # Not helpful!

✅ Good practice - clear, actionable messages:

try:
    flashcards = {"Dog": "Hund", "Cat": "Katze"}
    word = input("Enter a word: ")
    print(flashcards[word])
except KeyError:
    print(f"'{word}' not found. Available words: {', '.join(flashcards.keys())}")

Keep try blocks small - easier to debug

By keeping the try blocks small, you can handle different errors differently and make it clear which line caused the error. This makes your code easier to debug and prevents catching unrelated errors by accident (which should have been flagged and corrected).

❌ Bad practice - too much code in try block:

try:
    # Doing too many things at once
    name = input("Enter name: ")
    age = int(input("Enter age: "))
    height = float(input("Enter height: "))
    
    user_data = {"name": name, "age": age, "height": height}
    
    with open("users.txt", "a") as file:
        file.write(str(user_data))
    
    scores = [85, 92, 78]
    score = scores[age]
    
    print(f"Score: {score}")
except:
    print("Something failed!")  # Which line caused the error?

✅ Good practice - separate try blocks for different operations:

# Get user input with validation
try:
    age = int(input("Enter age: "))
except ValueError:
    print("Age must be a number!")
    age = 0

try:
    height = float(input("Enter height: "))
except ValueError:
    print("Height must be a number!")
    height = 0.0

# Access list with specific error handling
try:
    scores = [85, 92, 78]
    score = scores[age]
    print(f"Score: {score}")
except IndexError:
    print(f"No score available for age {age}")

# File operations separate
try:
    with open("users.txt", "a") as file:
        file.write(f"{age},{height}\n")
except FileNotFoundError:
    print("Could not save to file!")

Use else for success logic

Using else improves code readability by dedicating the try block to the risky operation and making it clear what happens on success, as opposed to failure.

✅ Good practice - separate error handling from success:

try:
    number = int(input("Enter a number: "))
except ValueError:
    print("Invalid number!")
else:
    # Only runs if conversion succeeded
    doubled = number * 2
    print(f"{number} × 2 = {doubled}")

Don’t hide errors you can’t handle

Unexpected errors should crash the program during development so that you can investigate and fix them. If you just use a catch all try statement and let the program continue, there is a good chance that your program doesn’t behave the way you expect it to and will not fulfil its requirements (or even cause harm).

❌ Bad practice - silently ignoring errors:

try:
    important_calculation()
except:
    pass  # Oops, we just hid a serious problem!

✅ Good practice - only catch errors you can handle:

try:
    user_age = int(input("Enter age: "))
except ValueError:
    print("Using default age of 18")
    user_age = 18
# Let other unexpected errors crash the program so we know about them!