Errors & Exceptions: When Things Go Wrong
1 · The lesson
readUp to now, when something went wrong in your code (dividing by zero, opening a missing file, converting "abc" to an integer), your program crashed with a red traceback.
This lesson shows you how to catch those errors, so your program can react to them instead of stopping.
This lesson covers the basics. The advanced topics (custom exceptions, exception chaining, exception groups) are in the Intermediate path.
1. The Two Kinds of "Errors"
Python has two kinds:
Syntax errors: Python can't even read (parse) your code. You see them as soon as you try to run it.
# print "hello" # SyntaxError, because Python 3 needs parentheses # if x = 5: # SyntaxError: = assigns, == compares
Syntax errors can't be caught at runtime. Fix them in your editor.
Exceptions: your code is valid, but something went wrong while it was running.
# print(10 / 0) # ZeroDivisionError # print(int("abc")) # ValueError # print(undefined_var) # NameError
Exceptions CAN be caught. That's what this lesson is about.
2. try / except: The Basic Shape
try: # code that might fail result = 10 / 0 except ZeroDivisionError: # what to do if it fails print("Can't divide by zero. Using 1 instead") result = 10 / 1 print(f"Result: {result}") # Result: 10.0
The try block holds the risky code. If any line in it raises an exception that matches the except, Python jumps to the except block. If nothing goes wrong, Python skips the except block.
3. Catching Specific Exceptions
Python has dozens of built-in exception types. These are the ones beginners see most often:
| Exception | When it happens |
|---|---|
ValueError | Conversion failed: int("abc"), float("xyz") |
TypeError | Wrong type for an operation: "5" + 1, calling a non-function |
ZeroDivisionError | Dividing by 0 |
KeyError | Asking a dict for a missing key |
IndexError | Asking a list for an out-of-range index |
FileNotFoundError | Opening a file that doesn't exist |
AttributeError | something.method() on an object that doesn't have it |
NameError | Using a variable that wasn't defined |
KeyboardInterrupt | User pressed Ctrl+C |
Always catch the most specific exception you can:
user_input = "not a number" try: n = int(user_input) print(f"You entered {n}") except ValueError: print("That wasn't a valid number. Please try again")
4. Catching Multiple Exception Types
You can list several exception types in one except, or have multiple except blocks:
data = {"name": "Alice"}
# Style A: several types in one tuple
try:
print(data["age"]) # KeyError
except (KeyError, IndexError):
print("Couldn't find that")
# Style B: different actions for different errors
try:
age = int(data["age"])
except KeyError:
print("No age field")
except ValueError:
print("Age wasn't a valid number")Use style B when each error needs a different response, style A when they share one.
5. The else and finally Clauses
The full shape of try/except has TWO extra clauses most beginners don't know about:
try: n = int("42") except ValueError: print("Conversion failed") else: # runs ONLY if no exception was raised print(f"Got {n}") finally: # ALWAYS runs, success or failure print("Cleaning up")
Output:
Got 42
Cleaning upelse is useful when you want to run code after the risky code succeeded, but you don't want that code inside the try (because it can't fail in the same way). This keeps the try block short.
finally is for cleanup that must always happen: closing a file, releasing a lock, logging that the work finished.
6. Why a Bare except: Is Bad
You CAN write except: with nothing after it. Don't do it. It catches every exception, including KeyboardInterrupt (so users can't stop your program with Ctrl+C) and SystemExit (which is how sys.exit() works).
# DON'T DO THIS try: risky() except: # catches EVERYTHING, which is bad pass # Slightly better: catches normal errors, but not Ctrl+C or sys.exit() try: risky() except Exception: # the catch-all for "normal" errors pass # BEST: catch only the errors you expect try: risky() except ValueError: handle_bad_value()
setup added so this can run · defines risky, handle_bad_value
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def risky(*_a, **_kw): print('-> risky() called') return _AutoMock('risky()') def handle_bad_value(*_a, **_kw): print('-> handle_bad_value() called') return _AutoMock('handle_bad_value()')
If you don't know what exception to expect, run the code without try/except first, see what it raises, then catch that specifically.
7. Reading the Exception: as e
You can capture the exception object to inspect it:
try: int("abc") except ValueError as e: print(f"Sorry, {e}") # Sorry, invalid literal for int() with base 10: 'abc' print(f"Error type: {type(e).__name__}") # ValueError
When you convert the exception to a string, you get its message. The exception object also has other attributes you can read.
8. Raising Exceptions Yourself
Sometimes you want your own code to report an error. Use raise:
def get_age(age): if age < 0: raise ValueError(f"Age can't be negative, got {age}") if age > 150: raise ValueError(f"That's not a realistic age: {age}") return age try: get_age(-5) except ValueError as e: print(f"Bad age: {e}") # Bad age: Age can't be negative, got -5
Custom exceptions (defining your own classes, chaining, adding context to messages) are covered in the Intermediate Exceptions lesson.
9. A Real Pattern: Safe Input Loop
This is the standard pattern for "ask the user for a number, and keep asking until they give a valid one":
answers = ["abc", "12.5"] # pretend the user typed these two answers, one after the other def ask_for_number(prompt): """Repeatedly ask until the user enters a valid number.""" while True: text = answers.pop(0) # in real code: text = input(prompt) try: return float(text) except ValueError: print(f"'{text}' isn't a number. Try again.") amount = ask_for_number("Enter an amount: ") print(f"You entered {amount}")
Output:
'abc' isn't a number. Try again. You entered 12.5
The first answer, "abc", raises a ValueError, so the except block prints a message and the while True loop asks again. The second answer is a valid number, so return ends the loop. In a real program, replace answers.pop(0) with input(prompt).
10. Mistakes You'll Hit
1. Bare except: (already covered, but worth repeating)
It catches Ctrl+C and sys.exit() too. You almost never want that.
2. Catching Exception and ignoring it (pass)
This hides bugs. If you catch an exception, at least report it:
try: risky() except Exception as e: print(f"Warning: {e}") # at least say something
setup added so this can run · defines risky
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def risky(*_a, **_kw): print('-> risky() called') return _AutoMock('risky()')
3. Putting too much in the try block
Put only the line that can fail inside try. Everything else goes outside the try, or in else:
# BAD: both lines can fail, in different ways try: data = load_file() total = sum(data) except: # which one failed? pass # GOOD: keep the try block small try: data = load_file() except FileNotFoundError: data = [] total = sum(data) # if data is bad, that error is a separate problem
setup added so this can run · defines load_file
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def load_file(*_a, **_kw): print('-> load_file() called') return _AutoMock('load_file()')
4. Using exceptions for normal flow control
Don't use try/except instead of an if statement when a normal if would work. Exceptions are for unusual situations.
🎯 Your Turn: A Calculator That Never Crashes
Users often type invalid input. If a program crashes on the first bad input, people
stop using it. Write safe_divide(a_text, b_text) that takes two strings and
returns either the result or a clear message. It must never raise an exception.
safe_divide("10", "4") → 2.5 safe_divide("10", "0") → "Cannot divide by zero." safe_divide("ten", "2") → "'ten' is not a number." safe_divide("10", "two") → "'two' is not a number."
Skeleton:
def safe_divide(a_text, b_text): # TODO 1: try converting BOTH strings to float # TODO 2: on ValueError, say which one was not a number # TODO 3: try the division # TODO 4: on ZeroDivisionError, return the friendly message ... print(safe_divide("10", "4")) # 2.5 print(safe_divide("10", "0")) # Cannot divide by zero. print(safe_divide("ten", "2")) # 'ten' is not a number.
Hint 1: Catch the specific exception, not everything
except ValueError catches a failed float() conversion.
except ZeroDivisionError catches division by zero. A bare
except: would also hide typos in your own code, and you would
never find out about them.
Hint 2: Convert one at a time to know which one failed
If you writefloat(a_text), float(b_text) in one line you know that
something failed but not which. Convert them in separate steps, or loop over
the two values, so your message can name the input that caused the error.
Show full solution
def safe_divide(a_text, b_text): numbers = [] for text in (a_text, b_text): try: numbers.append(float(text)) except ValueError: return f"'{text}' is not a number." try: return numbers[0] / numbers[1] except ZeroDivisionError: return "Cannot divide by zero." print(safe_divide("10", "4")) # 2.5 print(safe_divide("10", "0")) # Cannot divide by zero. print(safe_divide("ten", "2")) # 'ten' is not a number. print(safe_divide("10", "two")) # 'two' is not a number.
Two named exceptions give two useful messages. The loop lets the message name the
input that caused the error. "Invalid input" tells the user nothing, but
"'ten' is not a number" tells them exactly what to type again. Catching only
specific exceptions also keeps your own bugs visible: a misspelled variable in
this function still raises a NameError, as it
should.
What You Learned
- Syntax errors = code won't parse. Fix in editor.
- Exceptions = code parses but fails at runtime. Catch with
try/except. - Catch the most specific exception type.
except ValueError:beatsexcept Exception:beatsexcept:. - Full shape:
try / except [as e] / else / finally. - Use
raiseto signal errors in your own code. - Never use bare
except:. - Don't catch errors just to ignore them silently.
You can now write programs that keep running after bad input, missing files and invalid calculations. The next lesson, File I/O, uses these exception patterns a lot, because file operations often fail.
Source: adapted from Python official documentation Section 8 (Errors and Exceptions). PSF License.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.