Exception Handling in Python

Reviewed & published by Brayan K

Learn to detect, catch, and manage errors gracefully so your programs never crash unexpectedly.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

What Are Exceptions?

When something goes wrong in your Python code — like dividing by zero or opening a missing file — Python raises an exception. Without handling, your program crashes.

# This code will crash without exception handling
result = 10 / 0  # ZeroDivisionError!
print("This line never runs")

Why Handle Exceptions?

Without Exception HandlingWith Exception Handling
Program crashes completelyProgram continues running
Confusing error messagesUser-friendly messages
Data might be lostData is saved safely
Resources left openResources cleaned up properly

The try-except Block

Wrap risky code in a try block. If an error occurs, the except block handles it.

SyntaxWhat It Does
try:Code that might fail goes here
except:Runs if an error occurs in try
try:
    result = 10 / 0
    print("Result:", result)
except:
    print("Oops! Something went wrong.")

print("Program continues running!")

# ✅ Expected output:
# Oops! Something went wrong.
# Program continues running!

Catching Specific Exceptions

Always catch specific exception types. This is the professional way.

# Catching a specific exception type
try:
    number = int("hello")  # Can't convert "hello" to a number
except ValueError:
    print("That's not a valid number!")

# This only catches ValueError, not other errors

# ✅ Expected output:
# That's not a valid number!

Common Exception Types

Here are the exceptions you'll encounter most often:

ExceptionWhen It HappensExample
ValueErrorWrong value typeint("abc")
TypeErrorWrong operation for type"5" + 3
ZeroDivisionErrorDividing by zero10 / 0
IndexErrorInvalid list indexmy_list[100]
KeyErrorMissing dictionary keymy_dict["name"]
FileNotFoundErrorFile doesn't existopen("missing.txt")
NameErrorVariable not definedprint(xyz)
# Try different exceptions
my_list = [1, 2, 3]

try:
    print(my_list[10])  # IndexError
except IndexError:
    print("Index out of range!")

# Try this with a dictionary
my_dict = {"name": "Alice"}
try:
    print(my_dict["age"])  # KeyError
except KeyError:
    print("Key not found!")

# ✅ Expected output:
# Index out of range!
# Key not found!

Catching Multiple Exceptions

You can handle different exceptions differently:

user_input = "abc"  # Try changing to "0" or "5"

try:
    number = int(user_input)
    result = 100 / number
    print("Result:", result)
except ValueError:
    print("Please enter a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")

print("Program continues...")

# ✅ Expected output:
# Please enter a valid number!
# Program continues...

Or handle them together if the response is the same:

try:
    risky = 10 / int("zero")
except (ValueError, ZeroDivisionError):
    print("Invalid input - please try again!")

# ✅ Expected output:
# Invalid input - please try again!

Getting the Error Message

Use as to capture the error details:

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error occurred: {e}")
    # e contains the error message

# This is helpful for logging and debugging

# ✅ Expected output:
# Error occurred: division by zero

The else Block

The else block runs only if NO exception occurred:

BlockWhen It Runs
try:Always tries to run
except:Only if error in try
else:Only if NO error in try
number = "5"  # Try changing to "abc"

try:
    result = int(number)
except ValueError:
    print("Invalid number!")
else:
    print(f"Success! You entered: {result}")
    print(f"Doubled: {result * 2}")

# ✅ Expected output:
# Success! You entered: 5
# Doubled: 10

The finally Block

The finally block always runs, whether there was an error or not. Perfect for cleanup.

print("Starting operation...")

try:
    result = 10 / 2  # Try changing to 10 / 0
    print(f"Result: {result}")
except ZeroDivisionError:
    print("Cannot divide by zero!")
finally:
    print("Cleanup complete!")  # Always runs

print("Done!")

# ✅ Expected output:
# Starting operation...
# Result: 5.0
# Cleanup complete!
# Done!

The Complete Structure

All four blocks together:

try:
    # Risky code here
except SomeError:
    # Handle the error
else:
    # Runs if no error
finally:
    # Always runs (cleanup)
filename = "data.txt"

try:
    # Simulate file operation
    print(f"Trying to open {filename}...")
    # file = open(filename)  # Would cause FileNotFoundError
    data = "Sample data"  # Simulated success
except FileNotFoundError:
    print("File not found!")
    data = None
else:
    print("File read successfully!")
    print(f"Data: {data}")
finally:
    print("Operation complete - resources cleaned up.")

# ✅ Expected output:
# Trying to open data.txt...
# File read successfully!
# Data: Sample data
# Operation complete - resources cleaned up.

Worked Example: Watch Every Block Run

Reading the four blocks in a diagram is one thing; watching them fire in a real program is another. This function narrates itself. Run it and follow the order of the printed lines — that order is the whole lesson in one screen.

def divide(a, b):
    """Divide a by b, narrating every block as it runs."""
    try:
        result = a / b               # the risky line - it may explode
    except ZeroDivisionError as e:   # 'as e' captures the exception OBJECT
        print("  except  :", e)      # printing it shows Python's own message
        return None
    except TypeError as e:           # a second, different problem, handled separately
        print("  except  :", e)
        return None
    else:
        # 'else' runs only when the try block finished without an error.
        print("  else    : no error, result is", result)
        return result
    finally:
        # 'finally' runs no matter what - even after a return statement.
        print("  finally : always runs, error or not")

print("10 / 2")
print("returned:", divide(10, 2))    # the happy path: try -> else -> finally

print("10 / 0")
print("returned:", divide(10, 0))    # try -> except (ZeroDivisionError) -> finally

print("10 / 'x'")
print("returned:", divide(10, "x"))  # try -> except (TypeError) -> finally

# ✅ Expected output:
# 10 / 2
#   else    : no error, result is 5.0
#   finally : always runs, error or not
# returned: 5.0
# 10 / 0
#   except  : division by zero
#   finally : always runs, error or not
# returned: None
# 10 / 'x'
#   except  : unsupported operand type(s) for /: 'int' and 'str'
#   finally : always runs, error or not
# returned: None

Raising Exceptions

Use raise to trigger an exception yourself. This is useful for validating data.

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative!")
    if age > 150:
        raise ValueError("Age seems unrealistic!")
    return age

# Test it
try:
    my_age = set_age(-5)  # Try 25 or 200
except ValueError as e:
    print(f"Error: {e}")

# ✅ Expected output:
# Error: Age cannot be negative!

🎯 Your Turn: Catch It, Then Raise It

This function has to do both jobs: survive text that is not a number, and refuse an age that makes no sense. Three blanks are missing. Fill them in, run it, and match the expected output.

# 🎯 YOUR TURN — replace each ___ and run it.

def read_age(raw):
    try:
        age = int(raw)          # int("abc") cannot work - what does Python raise?
    except ___ as e:            # 👉 name that exception type here
        print("not a number:", e)
        return None

    if age < 0:
        ___ ValueError("age cannot be negative")   # 👉 the keyword that throws an error yourself

    return age

print(read_age("34"))     # a clean conversion
print(read_age("abc"))    # goes down the except branch, then returns None

try:
    read_age("-5")        # this one raises, so the caller has to catch it
except ValueError ___ err:    # 👉 how do you capture the exception object?
    print("caught:", err)

# ✅ Expected output:
# 34
# not a number: invalid literal for int() with base 10: 'abc'
# None
# caught: age cannot be negative

Common Mistakes to Avoid

MistakeProblemFix
except:Catches everything, hides bugsexcept ValueError:
Large try blockHard to know what failedPut only risky code in try
Empty except blockError is silently ignoredAt least log the error
Catching too highexcept Exception catches allBe specific about types
Not cleaning upFiles/connections left openUse finally or with
# BAD - Catches everything, hides bugs
try:
    result = 10 / my_variable  # NameError hidden!
except:
    print("Something went wrong")

# GOOD - Specific exceptions
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
# NameError will still crash (and we'll notice the bug!)

# ✅ Expected output:
# Something went wrong
# Cannot divide by zero

Practical Example: Safe Number Input

A function that safely gets a number from user input:

def get_number(prompt, default=0):
    """Safely get a number, return default if invalid."""
    user_input = input(prompt)  # In practice, this would be real input
    try:
        return float(user_input)
    except ValueError:
        print(f"Invalid input, using default: {default}")
        return default

# Simulate with different inputs
test_inputs = ["42", "3.14", "hello", ""]

for test in test_inputs:
    print(f"Input: '{test}'")
    try:
        result = float(test) if test else 0
        print(f"  → Got: {result}")
    except ValueError:
        print(f"  → Invalid, using default: 0")

Practical Example: Calculator

A safe calculator that handles errors:

def safe_calculate(a, b, operation):
    """Perform calculation with error handling."""
    try:
        if operation == "+":
            return a + b
        elif operation == "-":
            return a - b
        elif operation == "*":
            return a * b
        elif operation == "/":
            return a / b
        else:
            raise ValueError(f"Unknown operation: {operation}")
    except ZeroDivisionError:
        return "Error: Cannot divide by zero"
    except TypeError:
        return "Error: Invalid number type"

# Test the calculator
print(safe_calculate(10, 5, "+"))   # 15
print(safe_calculate(10, 5, "/"))   # 2.0
print(safe_calculate(10, 0, "/"))   # Error message
print(safe_calculate(10, 5, "^"))   # Unknown operation

Practical Example: Data Processor

Process a list of data, handling errors without stopping:

def process_data(data_list):
    """Process each item, skip invalid ones."""
    results = []
    errors = []
    
    for i, item in enumerate(data_list):
        try:
            # Try to convert to number and double it
            result = float(item) * 2
            results.append(result)
        except (ValueError, TypeError) as e:
            errors.append(f"Item {i}: {item} - {e}")
    
    return results, errors

# Test with mixed data
data = ["10", "5.5", "hello", "20", None, "15"]
results, errors = process_data(data)

print("Successful results:", results)
print("Errors encountered:")
for error in errors:
    print(f"  - {error}")

# ✅ Expected output:
# Successful results: [20.0, 11.0, 40.0, 30.0]
# Errors encountered:
#   - Item 2: hello - could not convert string to float: 'hello'
#   - Item 4: None - float() argument must be a string or a real number, not 'NoneType'

🎯 Mini-Challenge: Survive a Messy File

Real data is never clean. You are given five rows that should all be numbers — two of them are not. Your program must add up the good ones, report the bad ones, and never crash. Only the brief is given; the code is yours.

# 🎯 MINI-CHALLENGE: total the good rows, survive the bad ones
#
# 1. Start with:  rows = ["12", "7", "oops", "", "5"]
# 2. Keep a running total (starts at 0) and a skipped counter (starts at 0)
# 3. Loop over rows. Inside a try, convert the row with int() and add it to
#    the total.
# 4. Catch the ValueError. In the handler, print  skipped: 'oops'
#    (the row's own text inside single quotes) and add 1 to the skipped counter.
# 5. After the loop, print the total and then the skipped count.
#
# ✅ Expected output:
# skipped: 'oops'
# skipped: ''
# total: 24
# skipped count: 2

# your code here

Summary: Quick Reference

KeywordPurposeExample
try:Code that might failtry: x = 1/0
except Type:Handle specific errorexcept ValueError:
except Type as e:Capture error messageexcept ValueError as e:
else:Runs if no errorelse: print("OK")
finally:Always runs (cleanup)finally: file.close()
raiseTrigger an exceptionraise ValueError("!")

Key Takeaways:

Wrapping Up

You now know how to protect your programs from crashing and handle errors gracefully. Exception handling is essential for building reliable, professional Python applications.

Lesson done — your programs are now crash-proof!

try/except, else, finally, raise — you know the full exception handling toolkit. This is what separates fragile scripts from professional, production-ready code.

Practice quiz

Why does try/except exist?

  • To make code run faster
  • To catch errors so the program doesn't crash
  • To define new variables
  • To import modules

Answer: To catch errors so the program doesn't crash. try/except lets you handle errors gracefully so a problem doesn't crash the whole program.

Which exception is raised by int('hello')?

  • TypeError
  • KeyError
  • ValueError
  • IndexError

Answer: ValueError. Converting a non-numeric string with int() raises ValueError.

Which exception does accessing my_dict['age'] raise when that key is missing?

  • KeyError
  • IndexError
  • NameError
  • ValueError

Answer: KeyError. A missing dictionary key raises KeyError.

What does 10 / 0 raise?

  • ValueError
  • ZeroDivisionError
  • ArithmeticError only
  • Nothing, it returns infinity

Answer: ZeroDivisionError. Dividing by zero raises ZeroDivisionError.

When does the else block of a try statement run?

  • Always
  • Only if an exception occurred
  • Only if NO exception occurred in try
  • Never

Answer: Only if NO exception occurred in try. The else block runs only when the try block completed without raising an exception.

When does the finally block run?

  • Only on success
  • Only on error
  • Always, whether or not an error occurred
  • Only if there is no except block

Answer: Always, whether or not an error occurred. finally always runs — it's ideal for cleanup like closing files or connections.

How do you capture the error message in a variable named e?

  • except ValueError -> e:
  • except ValueError as e:
  • except ValueError(e):
  • except e = ValueError:

Answer: except ValueError as e:. Use 'except ValueError as e:' to bind the exception object to e.

Why is a bare 'except:' discouraged?

  • It is slower
  • It catches ALL errors and can hide bugs
  • It is invalid syntax
  • It only catches ValueError

Answer: It catches ALL errors and can hide bugs. A bare except catches everything, even unexpected errors, which can silently hide bugs.

How do you trigger an exception yourself for invalid data?

  • throw ValueError('bad')
  • raise ValueError('bad')
  • error ValueError('bad')
  • except ValueError('bad')

Answer: raise ValueError('bad'). The raise keyword triggers an exception, e.g. raise ValueError('Age cannot be negative!').

How can you handle ValueError and ZeroDivisionError the same way in one except?

  • except ValueError, ZeroDivisionError:
  • except (ValueError, ZeroDivisionError):
  • except ValueError or ZeroDivisionError:
  • except [ValueError, ZeroDivisionError]:

Answer: except (ValueError, ZeroDivisionError):. Group multiple exception types in a tuple: except (ValueError, ZeroDivisionError):.

Continue this course