Errors and Exceptions in Python
Reading tracebacks, the common exception types, handling errors with try, except, else and finally, raising your own exceptions, and writing programs that survive bad input.
When things go wrong at runtime
A program can be perfectly valid Python and still fail while it runs: the user types letters where a number belongs, a file is missing, a divisor turns out to be zero. Python signals such a problem by raising an exception, an object that describes what went wrong.
If nothing handles the exception, the program stops and prints a traceback. Read it from the bottom: the last line gives the exception's type and message, and the lines above show the chain of calls that led there, ending with the line that failed.
Exceptions are not only for crashes. They are Python's normal way of reporting "this operation could not be done", and handling them is part of writing reliable programs.
Exceptions you will meet most
| Exception | Raised when | Example |
|---|---|---|
| ValueError | The type is right but the value is unusable | int("12abc") |
| TypeError | An operation gets the wrong type | "3" + 4 |
| KeyError | A dictionary key is missing | {}["name"] |
| IndexError | A sequence index is out of range | [1, 2][5] |
| ZeroDivisionError | Dividing by zero | 10 / 0 |
| FileNotFoundError | Opening a file that does not exist | open("missing.txt") |
| AttributeError | A value has no such attribute or method | (5).upper() |
Handling an exception with try and except
Put the code that might fail in a try block and the recovery in an except block. If the try block raises an exception of the named type, Python jumps straight to the matching except block, runs it, and then carries on after the whole statement. If nothing goes wrong, the except block is skipped.
Lines in the try block after the failing line do not run. Keep the try block small, containing only the operation that can fail, so it is clear what you are protecting.
1for text in ["42", "forty-two", "7"]:2 try:3 number = int(text)4 print("Converted:", number)5 except ValueError:6 print("Not a whole number:", repr(text))7print("The program keeps going.")
Converted: 42 Not a whole number: 'forty-two' Converted: 7 The program keeps going.
Catching the right exceptions
Name the exception types you expect. A bare except: (or except Exception:) also catches mistakes you did not anticipate, including typos in your own code, and turns a clear error into a silent wrong result.
You can stack several except clauses for different problems, or group types in a tuple. except ValueError as error: binds the exception object, so you can show or log its message.
Exception types form a hierarchy: catching LookupError covers both KeyError and IndexError, and nearly everything inherits from Exception.
1def safe_ratio(values, i, j):2 try:3 return values[i] / values[j]4 except IndexError:5 return "index out of range"6 except ZeroDivisionError:7 return "cannot divide by zero"8 except TypeError as error:9 return f"bad data: {error}"1011data = [10, 0, "5"]12print(safe_ratio(data, 0, 3))13print(safe_ratio(data, 0, 1))14print(safe_ratio(data, 2, 0))15print(issubclass(KeyError, LookupError), issubclass(IndexError, LookupError))
index out of range cannot divide by zero bad data: unsupported operand type(s) for /: 'str' and 'int' True True
else and finally
A try statement has two more optional parts:
elseruns only if thetryblock finished without an exception. Put the code that depends on success there, outside the protected area.finallyruns always: after success, after a handled exception, and even when an unhandled exception is passing through or the function returns. It is for clean-up that must happen no matter what, such as closing a connection. (For files, thewithstatement in the next lesson does this for you.)
1def divide(a, b):2 try:3 result = a / b4 except ZeroDivisionError:5 print(" cannot divide by zero")6 return None7 else:8 print(" success")9 return result10 finally:11 print(" finished dividing", a, "by", b)1213print(divide(9, 3))14print(divide(1, 0))
success finished dividing 9 by 3 3.0 cannot divide by zero finished dividing 1 by 0 None
Ask permission or ask forgiveness?
There are two styles for dealing with operations that might fail:
- Look before you leap (LBYL): check first, then act.
if key in d: value = d[key]. - Easier to ask forgiveness than permission (EAFP): try it and handle the failure.
try: value = d[key] except KeyError: ....
Python code leans towards EAFP, especially when the check would duplicate the work of the operation itself. Deciding whether a string is a valid number, for example, is easiest by simply calling int(). LBYL remains the clearer choice for simple, cheap checks such as an empty list.
1settings = {"theme": "dark"}23if "font" in settings:4 font = settings["font"]5else:6 font = "default"7print(font)89try:10 theme = settings["theme"]11except KeyError:12 theme = "light"13print(theme)1415def is_number(text):16 try:17 float(text)18 return True19 except ValueError:20 return False2122print(is_number("3.5"), is_number("1e3"), is_number("abc"))
default dark True True False
Raising exceptions yourself
Your own functions should report invalid input the same way Python does: with raise. A function that receives an impossible value (a negative quantity, an unknown currency) should not print a message and return something odd; it should raise an exception with a clear message and let the caller decide what to do.
Choose a built-in type that fits: ValueError for a bad value, TypeError for the wrong type. Inside an except block, a bare raise re-raises the exception you just caught, which is useful when you want to log something and still let the error propagate.
1def withdraw(balance, amount):2 if amount <= 0:3 raise ValueError("amount must be positive")4 if amount > balance:5 raise ValueError(f"insufficient funds: balance is {balance}")6 return balance - amount78print(withdraw(100, 30))9for amount in (-5, 500):10 try:11 withdraw(100, amount)12 except ValueError as error:13 print("Refused:", error)
70 Refused: amount must be positive Refused: insufficient funds: balance is 100
Defining your own exception types
For errors that belong to your program's own rules, define a custom exception class. One line is enough: a class that inherits from Exception (or a more specific built-in). Callers can then catch exactly that error and nothing else. Classes are covered properly later; here, treat the line as a recipe.
1class OutOfStockError(Exception):2 pass34stock = {"pen": 3, "ink": 0}56def take(item):7 if stock.get(item, 0) == 0:8 raise OutOfStockError(f"{item} is out of stock")9 stock[item] -= 110 return item1112for item in ["pen", "ink", "pad"]:13 try:14 print("Took", take(item))15 except OutOfStockError as error:16 print("Sorry:", error)
Took pen Sorry: ink is out of stock Sorry: pad is out of stock
Pattern: asking until the input is valid
Interactive programs combine a loop with exception handling: keep asking until the answer converts, then break out. In a real program the loop body calls input(); the example feeds a list of typed answers instead, so it can run here.
while True:
text = input("How many tickets? ")
try:
count = int(text)
except ValueError:
print("Please type a whole number.")
continue
if count < 1:
print("At least one ticket.")
continue
break
1answers = iter(["two", "-1", "3"])23while True:4 text = next(answers)5 print("User typed:", text)6 try:7 count = int(text)8 except ValueError:9 print(" Please type a whole number.")10 continue11 if count < 1:12 print(" At least one ticket.")13 continue14 break15print("Booking", count, "tickets")
User typed: two Please type a whole number. User typed: -1 At least one ticket. User typed: 3 Booking 3 tickets
Debugging habits
Handling exceptions keeps a program running; debugging finds out why it failed. A few habits save hours:
- Read the whole last line of the traceback and the line number above it before changing anything.
- Reproduce with the smallest input that still fails.
- Print intermediate values (an f-string with
=such asprint(f"{total=}")shows name and value) or step through with a debugger. - Check assumptions with
assert:assert len(rows) > 0, "no rows loaded"stops immediately with your message when an assumption is false. Use it for programmer errors, not for validating user input.
- ✓Catch specific exceptions close to where you can do something useful about them.
- ✓Give raised exceptions messages that say what was wrong and with which value.
- ✓Let unexpected exceptions crash during development so you see them.
- ✕Write except: pass, which hides every error, including your own bugs.
- ✕Wrap a whole program in one giant try block.
- ✕Use exceptions for ordinary control flow when a simple if is clearer.
Check your understanding
Which exception does `int("3.5")` raise?
Practice
Parse a list of typed numbers without crashing on the bad ones, then clean and aggregate a CSV export that contains unusable rows.
Safe Integer Parser
Reveal Solution & AnalysisHide Solution
Pandas-Style Sales Analysis
Reveal Solution & AnalysisHide Solution
Quick Syntax & Reference Sheet
| Concept / Rule | Syntax / Usage |
|---|---|
| Handle one error | try:
n = int(text)
except ValueError:
n = 0 |
| Get the message | except ValueError as error: print(error) |
| Several types | except (KeyError, IndexError): |
| Success / always | else: ... finally: ... |
| Raise | raise ValueError("amount must be positive") |
| Re-raise after logging | except Exception:
log(...)
raise |
| Custom exception | class OutOfStockError(Exception): pass |
Common Pitfalls & Developer Mistakes
Catching everything with a bare except
Why it happens: Real bugs (typos, wrong types) are hidden along with the expected error.
Putting too much code in the try block
Why it happens: You cannot tell which line failed, and you may catch an error from code you did not mean to protect.
Printing an error and returning a fake value
Why it happens: Callers continue with wrong data.
Using assert to validate user input
Why it happens: Assertions can be switched off with python -O, and they are meant for programmer mistakes.