Exceptions
Learn how to handle errors in Python using try, except, else, finally, and raise.
Exceptions
Things go wrong in programs. A file does not exist. A user types a letter where you expected a number. A network request fails. These are called exceptions — events that interrupt the normal flow of your program.
Without handling them, your program crashes and shows an ugly error message. With proper exception handling, you can catch the problem, respond to it gracefully, and keep your program running.
# without handling — program crashes
number = int(input("Enter a number: ")) # user types "hello"
# ValueError: invalid literal for int() with base 10: 'hello'
# with handling — program responds gracefully
try:
number = int(input("Enter a number: "))
print(f"You entered: {number}")
except ValueError:
print("That is not a valid number. Please try again.")Common built-in exceptions
Before handling exceptions, know what you are dealing with:
| Exception | When it happens |
|---|---|
ValueError | Right type, wrong value — int("hello") |
TypeError | Wrong type — "text" + 5 |
IndexError | List index out of range — items[99] |
KeyError | Dictionary key not found — d["missing"] |
AttributeError | Object has no such attribute — "hi".push() |
FileNotFoundError | File does not exist — open("nope.txt") |
ZeroDivisionError | Dividing by zero — 10 / 0 |
NameError | Variable not defined — print(x) before x = ... |
ImportError | Module not found — import nope |
StopIteration | Iterator has no more items |
RecursionError | Too many nested function calls |
PermissionError | No permission to access a file |
TimeoutError | Operation took too long |
MemoryError | Not enough memory |
try and except — the basics
Wrap risky code in a try block. If an exception happens, Python jumps to the except block:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero.")Output:
Cannot divide by zero.Without the try/except, this would crash. With it, the error is caught and handled.
How try/except/else/finally flows
try— the code you want to runexcept— runs only if an exception was raisedelse— runs only if NO exception was raisedfinally— always runs no matter what
Catching specific exceptions
Always catch the most specific exception you can. Do not catch everything blindly:
try:
age = int(input("Enter your age: "))
result = 100 / age
print(f"100 divided by your age is {result:.2f}")
except ValueError:
print("Please enter a valid number.")
except ZeroDivisionError:
print("Age cannot be zero.")Python checks each except from top to bottom and runs the first one that matches.
Catching multiple exceptions in one line
try:
value = int(input("Enter a number: "))
result = 10 / value
except (ValueError, ZeroDivisionError):
print("Invalid input — enter a non-zero number.")Getting the exception details
Use as to capture the exception object and read its message:
try:
number = int("hello")
except ValueError as e:
print(f"Error: {e}")
# Error: invalid literal for int() with base 10: 'hello'try:
with open("missing.txt", "r") as f:
content = f.read()
except FileNotFoundError as e:
print(f"File error: {e}")
# File error: [Errno 2] No such file or directory: 'missing.txt'else — runs when no exception occurred
The else block runs only if the try block completed without any exception. It is the right place for code that should only run on success:
try:
age = int(input("Enter your age: "))
except ValueError:
print("That is not a valid number.")
else:
# only runs if int() succeeded
if age >= 18:
print("You are an adult.")
else:
print("You are a minor.")Why use else instead of just putting the code at the end of the try block? Because code in else is protected — if it raises an exception, it will NOT be caught by the except above it. This prevents accidentally hiding bugs.
finally — always runs
finally runs no matter what — whether an exception happened or not, whether it was caught or not. Use it for cleanup — closing files, releasing resources, closing database connections:
try:
file = open("data.txt", "r")
content = file.read()
result = int(content)
except FileNotFoundError:
print("File not found.")
except ValueError:
print("File does not contain a valid number.")
finally:
print("Cleaning up...")
# this always runsA common real pattern — ensure a resource is released:
connection = None
try:
connection = connect_to_database()
data = connection.fetch("SELECT * FROM users")
except DatabaseError as e:
print(f"Database error: {e}")
finally:
if connection:
connection.close() # always close the connectionIn modern Python you rarely need finally for file handling because with handles closing automatically. But for databases, network connections, and other resources, finally is still essential.
Catching all exceptions
You can catch any exception with a bare except Exception:
try:
risky_operation()
except Exception as e:
print(f"Something went wrong: {e}")Be careful with catching all exceptions. It can hide bugs — you might swallow an error you did not expect and have no idea why your program is not working correctly. Always prefer catching specific exceptions. Only catch Exception at the top level of your program where you want to prevent crashes and log the error.
Never use a bare except: without Exception — it catches even system exits and keyboard interrupts:
# bad — catches Ctrl+C and sys.exit() too
try:
risky()
except:
pass
# better — only catches real exceptions
try:
risky()
except Exception as e:
print(f"Error: {e}")raise — throwing exceptions manually
You can raise exceptions yourself when something goes wrong in your own logic:
def set_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"Age must be between 0 and 150, got {age}")
return age
try:
set_age(-5)
except ValueError as e:
print(f"Invalid age: {e}")Output:
Invalid age: Age must be between 0 and 150, got -5raise from — exception chaining
When you catch one exception and raise another, use raise from to keep the original context:
def load_config(path):
try:
with open(path, "r") as f:
return json.load(f)
except FileNotFoundError as e:
raise RuntimeError(f"Config file missing: {path}") from eThe original FileNotFoundError is preserved as context — when you see the traceback, you see both exceptions. This makes debugging much easier.
If you deliberately want to hide the original exception:
raise RuntimeError("Config failed") from Nonere-raising an exception
Catch an exception, do something with it, then re-raise it to let it bubble up:
def process_file(path):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError as e:
log_error(f"File not found: {path}") # log it
raise # re-raise the original exceptionPlain raise with no argument re-raises the current exception exactly as it was.
Nested try/except
You can nest try blocks inside each other:
def get_user_score():
try:
raw = input("Enter your score (0-100): ")
try:
score = int(raw)
except ValueError:
print(f"'{raw}' is not a number. Using 0.")
score = 0
if score < 0 or score > 100:
raise ValueError(f"Score must be 0-100, got {score}")
return score
except ValueError as e:
print(f"Invalid score: {e}")
return NoneA real example — a robust file reader
import json
from pathlib import Path
def load_json_file(filepath):
"""
Load a JSON file safely.
Returns the data or None if anything goes wrong.
"""
path = Path(filepath)
try:
if not path.exists():
raise FileNotFoundError(f"File not found: {filepath}")
if path.suffix != ".json":
raise ValueError(f"Expected a .json file, got {path.suffix}")
content = path.read_text(encoding="utf-8")
data = json.loads(content)
except FileNotFoundError as e:
print(f"File error: {e}")
return None
except ValueError as e:
print(f"Format error: {e}")
return None
except json.JSONDecodeError as e:
print(f"JSON parse error in {filepath}: {e}")
return None
except PermissionError:
print(f"No permission to read {filepath}")
return None
else:
print(f"Successfully loaded {filepath}")
return data
finally:
print(f"Finished processing {filepath}")
data = load_json_file("config.json")
if data:
print(data)Exception hierarchy
Python's built-in exceptions follow an inheritance tree. When you catch a parent class, you catch all its children too:
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
├── ValueError
├── TypeError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── LookupError
│ ├── IndexError
│ └── KeyError
└── ... (many more)So catching OSError catches FileNotFoundError, PermissionError, and TimeoutError all at once:
try:
with open("data.txt", "r") as f:
content = f.read()
except OSError as e:
print(f"File system error: {e}")Summary
| Keyword | Purpose |
|---|---|
try | Wrap risky code |
except ErrorType | Handle a specific exception |
except (A, B) | Handle multiple exceptions |
except ErrorType as e | Capture exception details |
else | Run if no exception occurred |
finally | Always runs — cleanup code |
raise ErrorType("msg") | Raise an exception manually |
raise | Re-raise the current exception |
raise X from Y | Chain exceptions with context |