Skip to main content

Error Handling in Python

Calculating read time…

In the previous lesson, you learned how to package logic into reusable functions. Now let's make those functions bulletproof — so a single bad input doesn't crash your entire program.

Imagine you're building a house of cards. One wrong move and everything falls down. Wouldn't it be great if you could gently catch a falling card and place it back, instead of watching the whole tower collapse?

That's exactly what error handling does in Python! It lets your programs handle mistakes gracefully, so small errors don't crash everything.

Today, we'll learn how to use try...except to make your Python programs strong, reliable, and user-friendly. You'll go from zero to hero by the end of this tutorial.

💡 What You'll Master Today:
  • The difference between syntax errors and runtime exceptions
  • Basic try...except blocks — and why bare except: is risky
  • Catching specific exceptions like ValueError and ZeroDivisionError
  • The full toolkit: try, except, else, and finally
  • Creating your own custom exception classes
  • Python's real exception hierarchy (and why it matters for bare except:)
  • Two full projects: a safe file reader and a simulated Weather API client

🚧 1. What Are Errors, Really?

In Python, errors are like road signs telling you something went wrong. They stop your program completely unless you handle them.

Think of errors as your program's way of saying, "Hey, I don't know what to do here!"

Two Main Types of Errors

Syntax Errors: Like spelling mistakes in your code. Python catches these before running your program.

Runtime Errors (Exceptions): Happen while your program is running, often due to unexpected situations.

We'll focus on handling runtime errors today.

🎯 2. The Magic Words: Try and Except

The try...except block is Python's safety net. It lets you "try" risky code and "except" (catch) errors if they occur.

💡 Analogy Time: Think of try as saying, "Let me try to pour this juice." except is having a towel ready in case you spill. The towel catches the mess so your whole kitchen doesn't get sticky!

Basic Structure

📌 What this code does: nothing runs yet — this is the skeleton every try...except block follows. Python runs the try section first, and only jumps into except if something inside try raises an error.
try:
    # Risky code goes here
    # This is where things might break
except:
    # What to do IF something breaks
    # This is your safety plan

🔰 3. Your First Try...Except Block

Let's start with a common beginner error: dividing by zero.

📌 What this code does: attempts to divide 10 by 0 with no protection at all — Python has no choice but to crash the entire program the moment it hits that line, so the final print() never runs.
# Without error handling
print("Let's do some math!")
result = 10 / 0  # This will crash!
print("The result is:", result)

Run this and you'll see: ZeroDivisionError: division by zero

Now, let's fix it with our safety net:

📌 What this code does: wraps the exact same risky division inside a try block. When it fails, Python jumps straight to except instead of crashing — and the program keeps running afterward, proven by the final print statement still executing.
# WITH error handling
try:
    print("Let's do some math!")
    result = 10 / 0
    print("The result is:", result)
except:
    print("Oops! You can't divide by zero.")
    print("The program continues happily instead of crashing!")

print("See? We're still running! 🎉")

Output (verified):

Let's do some math!
Oops! You can't divide by zero.
The program continues happily instead of crashing!
See? We're still running! 🎉
✅ DO: Always wrap risky operations in try blocks. Think of operations involving user input, file reading, or calculations that could fail.
❌ DON'T: Don't wrap your entire program in one big try block. You won't know where errors actually happened!

🎯 4. Catching Specific Errors

Blanket catching (just except:) works, but it's like catching ALL falling objects with one net. Better to catch specific errors separately.

Common Python Errors to Know:

  • ZeroDivisionError → Dividing by zero
  • ValueError → Wrong type of value (like converting "hello" to a number)
  • TypeError → Wrong operation on a type (like adding string to number)
  • FileNotFoundError → Trying to open a file that doesn't exist
  • IndexError → Accessing a list index that doesn't exist
  • KeyError → Accessing a dictionary key that doesn't exist

Example: Catching Different Errors

📌 What this code does: asks the user for a number and divides 100 by it, but handles two specific failure modes separately — dividing by zero and typing something that isn't a number at all — with a generic catch-all as a last resort.
def safe_calculator():
    try:
        num = int(input("Enter a number: "))
        result = 100 / num
        print(f"100 divided by {num} is {result}")
    
    except ZeroDivisionError:
        print("You can't divide by zero! Please try again.")
    
    except ValueError:
        print("That's not a valid number! Please enter digits only.")
    
    except:
        print("Something unexpected went wrong!")

# Let's test it!
safe_calculator()
⚠️ IMPORTANT: Order matters! Put specific exceptions first, generic ones last. Python checks exceptions in order.

🧰 5. The Full Error Handling Toolkit

Meet the complete team: try, except, else, and finally.

Complete Structure

📌 What this code does: nothing executes yet — it's the full template. Python checks each except in order until one matches; else runs only if nothing in try failed; finally always runs last, no matter what happened above it.
try:
    # Try this risky code
    print("Trying something risky...")
    
except SpecificError:
    # Handle this specific error
    print("Caught a specific error!")
    
except AnotherError:
    # Handle another specific error
    print("Caught another error!")
    
except:
    # Handle any other errors (general catch)
    print("Caught an unexpected error!")
    
else:
    # Run ONLY if NO errors occurred in try block
    print("Success! No errors!")
    
finally:
    # Run ALWAYS, no matter what (error or no error)
    print("This always runs, like cleaning up.")

Real-World Example: User Registration

Let's build a user registration system with complete error handling:

📌 What this code does: collects name/age/email, then manually raises a ValueError with a specific message the moment any validation rule fails — that single except ValueError as e: clause catches all three checks at once, printing whichever message was actually raised.
def register_user():
    users = []  # Our "database"
    
    try:
        print("=== User Registration ===")
        name = input("Enter your name: ").strip()
        age = int(input("Enter your age: "))
        email = input("Enter your email: ").strip()
        
        # Some validation
        if not name:
            raise ValueError("Name cannot be empty!")
        
        if age < 13:
            raise ValueError("You must be 13 or older!")
        
        if "@" not in email:
            raise ValueError("Invalid email format!")
    
    except ValueError as e:
        print(f"Registration failed: {e}")
        print("Please try again with valid information.")
        return False
    
    except KeyboardInterrupt:
        print("\nRegistration cancelled by user.")
        return False
    
    except:
        print("An unexpected error occurred during registration.")
        return False
    
    else:
        # If we get here, everything worked!
        users.append({"name": name, "age": age, "email": email})
        print(f"Welcome, {name}! Registration successful! 🎉")
        return True
    
    finally:
        print("=== Registration process completed ===")

# Test our function
register_user()
🔬 Worth knowing: the except KeyboardInterrupt: line here is actually meaningful, not decorative — KeyboardInterrupt lives directly under BaseException, not Exception (confirmed later in this post's hierarchy section), so a bare except: below it would also catch a Ctrl+C. Listing it explicitly, as done here, makes the intent clear to anyone reading the code later: "yes, we deliberately want to catch this too."

🏗️ 6. Advanced Technique: Custom Exceptions

Sometimes Python's built-in errors aren't specific enough. You can create your own!

Creating Custom Errors

📌 What this code does: defines two brand-new exception types by inheriting from Exception, then raises the appropriate one based on which validation rule fails — giving each failure a precise, self-documenting name instead of a generic ValueError.
# Define your own error class
class TooYoungError(Exception):
    """Raised when user is too young"""
    pass

class InvalidEmailError(Exception):
    """Raised when email format is invalid"""
    pass

# Using custom errors
def check_registration(age, email):
    if age < 18:
        raise TooYoungError("Must be 18 or older!")
    
    if "@" not in email or "." not in email:
        raise InvalidEmailError("Please enter a valid email address!")
    
    return True

# Testing it
try:
    check_registration(16, "test@example")
except TooYoungError as e:
    print(f"Age issue: {e}")
except InvalidEmailError as e:
    print(f"Email issue: {e}")

Output (verified):

Age issue: Must be 18 or older!
✅ DO: Create custom exceptions for your application's specific needs. It makes debugging much easier!

📄 7. Practical Project: File Reader with Error Handling

Let's build a file reader that handles every possible error gracefully.

📌 What this code does: tries to open and read a file, with five separate except clauses handling five distinct real-world failure modes (missing file, no permission, path is actually a folder, unreadable byte encoding, and anything else) — plus else for success and finally to always confirm the attempt is over.
def read_file_safely(filename):
    """Read a file with complete error handling"""
    
    try:
        print(f"Attempting to read: {filename}")
        
        with open(filename, 'r') as file:
            content = file.read()
    
    except FileNotFoundError:
        print(f"Error: File '{filename}' not found.")
        print("Please check the filename and try again.")
        return None
    
    except PermissionError:
        print(f"Error: No permission to read '{filename}'.")
        return None
    
    except IsADirectoryError:
        print(f"Error: '{filename}' is a directory, not a file.")
        return None
    
    except UnicodeDecodeError:
        print(f"Error: Cannot decode file '{filename}'.")
        print("The file might be binary or in an unknown format.")
        return None
    
    except:
        print("An unexpected error occurred while reading the file.")
        return None
    
    else:
        print("File read successfully!")
        return content
    
    finally:
        print("File operation completed.")

# Test with different scenarios
read_file_safely("my_data.txt")       # Normal file
read_file_safely("missing.txt")       # Non-existent file
read_file_safely("/root/private.txt") # Permission issue (on Linux/Mac)

Output (verified on a real filesystem):

Attempting to read: my_data.txt
File read successfully!
File operation completed.

Attempting to read: missing.txt
Error: File 'missing.txt' not found.
Please check the filename and try again.
File operation completed.

Attempting to read: /root/private.txt
Error: No permission to read '/root/private.txt'.
File operation completed.
🔬 Verified correction: a version of this example that tests read_file_safely("/etc/passwd") and labels it "Permission issue (on Linux/Mac)" is factually wrong. We checked real file permissions: /etc/passwd is -rw-r--r-- — world-readable by design on every standard Unix system, precisely because password hashes actually live in the separately-locked-down /etc/shadow file, not in /etc/passwd. Running that line will print "File read successfully!", not trigger a PermissionError. We've swapped in /root/private.txt above (a path an unprivileged user genuinely cannot read on most systems) so the demo actually behaves as described. If you want to test this yourself, create a file and run chmod 000 yourfile.txt first.

✅ 8. Error Handling Best Practices

1. Be Specific with Exceptions

📌 What this code does: compares catching one exact, expected error type versus catching literally anything with a bare except: — the second version can't tell you what actually went wrong.
# 👍 GOOD
try:
    value = int(user_input)
except ValueError:
    print("Please enter a valid number")

# 👎 BAD
try:
    value = int(user_input)
except:
    print("Error")  # Too vague!

2. Don't Hide Errors

📌 What this code does: catches an error, actually reports and logs it, and — because it's serious — raises it again so it doesn't vanish silently, versus the bad version that swallows the error with pass and leaves no trace it ever happened.
# 👍 GOOD
try:
    result = risky_operation()
except Exception as e:
    print(f"Error occurred: {e}")
    # Log the error for debugging
    log_error(e)
    # Maybe re-raise if it's critical
    raise

# 👎 BAD
try:
    result = risky_operation()
except:
    pass  # Silent failure - dangerous!

3. Use Finally for Cleanup

📌 What this code does: guarantees the file gets closed in finally whether the read succeeds or fails — cleanup code that absolutely must run belongs here, not just at the end of the try block (where it would get skipped if an error occurred first).
file = None
try:
    file = open("data.txt", "r")
    # Process file
except:
    print("Error processing file")
finally:
    if file:
        file.close()  # Always close the file!
    print("Cleanup completed")
🔬 Modern alternative: the pattern above is correct, but in real code you'll almost always prefer a with open(...) as file: block instead (as used earlier in this post's file-reader project) — it closes the file automatically even on error, without needing a manual finally at all.

🌦️ 9. Real-World Application: Weather API Call

Here's a complete example combining everything we've learned:

📌 What this code does: simulates fetching weather data for a city, layering five different except clauses — input validation, a custom "city not found" error, a network failure, a real requests library error, and a final catch-all — to show how a production API client typically separates every category of thing that can go wrong.
import requests

class WeatherAPIError(Exception):
    """Custom error for weather API issues"""
    pass

def get_weather(city):
    """Get weather for a city with robust error handling"""
    
    try:
        print(f"Fetching weather for {city}...")
        
        # Simulate API call (in real life, use actual API)
        if not city:
            raise ValueError("City name cannot be empty!")
        
        if city.lower() == "atlantis":
            raise WeatherAPIError("City not found in our database!")
        
        # Simulate network issues
        if city.lower() == "disconnected":
            raise ConnectionError("Network connection failed!")
        
        # Simulate successful response
        weather_data = {
            "temperature": 22,
            "conditions": "Sunny",
            "humidity": 65
        }
        
    except ValueError as e:
        print(f"Input error: {e}")
        return None
    
    except WeatherAPIError as e:
        print(f"Weather API error: {e}")
        return None
    
    except ConnectionError:
        print("Network error: Please check your internet connection.")
        return None
    
    except requests.exceptions.RequestException:
        print("API request failed. The weather service might be down.")
        return None
    
    except:
        print("An unexpected error occurred while fetching weather.")
        return None
    
    else:
        print("Weather data fetched successfully!")
        return weather_data
    
    finally:
        print("Weather check completed.")

# Test various scenarios
print(get_weather("New York"))
print(get_weather(""))  # Empty city
print(get_weather("Atlantis"))  # Non-existent city

Output (verified):

Fetching weather for New York...
Weather data fetched successfully!
Weather check completed.
{'temperature': 22, 'conditions': 'Sunny', 'humidity': 65}
Fetching weather for ...
Input error: City name cannot be empty!
Weather check completed.
None
Fetching weather for Atlantis...
Weather API error: City not found in our database!
Weather check completed.
None
🔬 A subtle gotcha worth knowing: we checked this — requests.exceptions.ConnectionError does not inherit from Python's builtin ConnectionError. They're unrelated classes that happen to share a name; the requests version actually inherits from requests.exceptions.RequestException. In this simulation it doesn't matter, since the code manually raises the builtin ConnectionError — but if you swap in a real requests.get() call later, a genuine network failure will skip the except ConnectionError: clause entirely and get caught by except requests.exceptions.RequestException: instead. The ordering above still works correctly for that reason — just don't assume the two "ConnectionError" names mean the same class.

📚 10. Quick Reference Cheat Sheet

Basic Patterns

# 1. Basic catch-all
try:
    risky_code()
except:
    handle_error()

# 2. Catch specific error
try:
    risky_code()
except ValueError:
    handle_value_error()

# 3. Catch multiple errors
try:
    risky_code()
except (ValueError, TypeError):
    handle_either_error()

# 4. Full pattern
try:
    risky_code()
except SpecificError:
    handle_error()
else:
    no_error_code()
finally:
    always_run_code()

🌳 11. Common Exception Hierarchy

We verified this tree directly against Python's actual class hierarchy (via __mro__) — it's accurate, including the detail most tutorials get wrong: KeyboardInterrupt and SystemExit sit directly under BaseException, not under Exception.

  • BaseException (Parent of all)
    • Exception (What we usually catch)
      • ArithmeticError (Math errors)
        • ZeroDivisionError
      • LookupError
        • IndexError (list out of range)
        • KeyError (missing dict key)
      • OSError (File/OS errors)
        • FileNotFoundError
        • PermissionError
        • IsADirectoryError
      • ValueError (Wrong value type)
        • UnicodeDecodeError
      • TypeError (Wrong operation)
    • KeyboardInterrupt (Ctrl+C)
    • SystemExit (Program exit)
💡 PRO TIP: Catch Exception, not BaseException, unless you have a specific reason. This avoids catching system-level interrupts.
🔬 Why this tree matters in practice: because KeyboardInterrupt lives directly under BaseException and not Exception, a bare except: (which secretly means "catch BaseException") will swallow a user's Ctrl+C entirely — your program will just print your generic error message and keep running instead of stopping. Writing except Exception: instead fixes this, because Exception doesn't include KeyboardInterrupt or SystemExit.

🚧 12. Common Pitfalls to Avoid

❌ DON'T: Don't use bare except clauses (except:) in production code. You might catch unexpected system errors like KeyboardInterrupt (Ctrl+C).
📌 What this code does: shows the risky bare-except version next to the safer, more precise fix.
# Bad — silently swallows Ctrl+C along with real bugs
try:
    risky_code()
except:
    print("Something went wrong")

# Good — catches ordinary errors, lets Ctrl+C through
try:
    risky_code()
except Exception as e:
    print(f"Something went wrong: {e}")
❌ DON'T: Don't handle errors too early. Let functions fail naturally, then handle errors at the right level.
❌ DON'T: Don't ignore exceptions with empty except blocks. At minimum, log the error.
❌ DON'T: Don't assume a same-named exception from a third-party library (like requests.exceptions.ConnectionError) is the same class as a Python builtin with the same name — check the library's docs or run ExceptionClass.__mro__ yourself to confirm.

🏋️ 13. Practice Exercises

✅ Exercise 1: Safe List Access
Write a function that safely retrieves an item at a given index from a list, catching IndexError and returning a default value like None instead of crashing.
✅ Exercise 2: Dictionary Lookup with KeyError
Write a function that safely looks up a value in a dictionary by key, catching KeyError and printing a friendly "key not found" message.
✅ Exercise 3: Custom Exception for Negative Numbers
Create a NegativeNumberError custom exception and raise it inside a square-root function whenever the input is negative.
✅ Exercise 4: Fix the Permission Test
Using the corrected file-reader project from this post, create a file, run chmod 000 on it (Linux/Mac), and confirm your program actually raises PermissionError — then explain in a comment why /etc/passwd would not have triggered the same error.
✅ Exercise 5: Retry Logic
Write a function that attempts a risky operation up to 3 times using a loop and try...except, only giving up and re-raising the error after the third failed attempt.

❓ 14. Frequently Asked Questions

What is the difference between a syntax error and an exception in Python?
A syntax error means Python can't even parse your code — it's caught before the program runs at all. An exception (runtime error) happens while the program is executing, often due to unexpected input or conditions, and can be caught and handled with try...except.

Why shouldn't I use a bare except: clause?
A bare except: catches BaseException, which includes KeyboardInterrupt (Ctrl+C) and SystemExit — meaning your program could silently swallow a user's attempt to stop it. Use except Exception: instead, which excludes those system-level signals.

What's the difference between except and finally?
except only runs if a matching error actually occurred. finally always runs, whether an error occurred or not — it's the right place for cleanup code like closing files or network connections.

When does the else block run in a try/except statement?
Only when the try block completes with no exceptions at all. It's useful for code that should run only on success, keeping it separate from the risky code in try.

Should I create custom exceptions for my own code?
Yes, when built-in exceptions aren't descriptive enough for your application's specific rules. A custom exception like TooYoungError is far clearer to future readers (and to your own error logs) than a generic ValueError.

Are KeyboardInterrupt and SystemExit subclasses of Exception?
No — this is a common misconception. Both inherit directly from BaseException, not Exception. That's exactly why except Exception: is safer than a bare except: for everyday error handling.

📝 15. Summary

You now have a complete Python error-handling toolkit:

  • try/except → catch runtime errors instead of crashing
  • Specific exceptions → catch ValueError, ZeroDivisionError, etc. separately for precise handling
  • else/finally → run code only on success, or always, respectively
  • Custom exceptions → name your own application-specific error conditions
  • The exception hierarchy → why Exception, not BaseException, is almost always what you want to catch
  • Real projects → a safe file reader and a simulated Weather API client tie every technique together

Happy coding! 💻✨

Comments