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.
- The difference between syntax errors and runtime exceptions
- Basic
try...exceptblocks — and why bareexcept:is risky - Catching specific exceptions like
ValueErrorandZeroDivisionError - The full toolkit:
try,except,else, andfinally - 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?
- 2. The Magic Words: Try and Except
- 3. Your First Try...Except Block
- 4. Catching Specific Errors
- 5. The Full Error Handling Toolkit
- 6. Advanced Technique: Custom Exceptions
- 7. Practical Project: File Reader with Error Handling
- 8. Error Handling Best Practices
- 9. Real-World Application: Weather API Call
- 10. Quick Reference Cheat Sheet
- 11. Common Exception Hierarchy
- 12. Common Pitfalls (And How to Avoid Them)
- 13. Practice Exercises
- 14. FAQ
- 15. Summary
🚧 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.
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
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.
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:
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! 🎉
🎯 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 zeroValueError→ 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 existIndexError→ Accessing a list index that doesn't existKeyError→ Accessing a dictionary key that doesn't exist
Example: Catching Different Errors
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()
🧰 5. The Full Error Handling Toolkit
Meet the complete team: try, except, else, and finally.
Complete Structure
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:
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()
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
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!
📄 7. Practical Project: File Reader with Error Handling
Let's build a file reader that handles every possible error gracefully.
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.
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
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
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
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")
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:
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
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)
- ArithmeticError (Math errors)
- KeyboardInterrupt (Ctrl+C)
- SystemExit (Program exit)
- Exception (What we usually catch)
Exception, not BaseException, unless you have a specific reason. This avoids catching system-level interrupts.
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
except:) in production code. You might catch unexpected system errors like KeyboardInterrupt (Ctrl+C).
# 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}")
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
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.
Write a function that safely looks up a value in a dictionary by key, catching
KeyError and printing a friendly "key not found" message.
Create a
NegativeNumberError custom exception and raise it inside a square-root function whenever the input is negative.
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.
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, notBaseException, 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
Post a Comment