Advanced Python Techniques
Exception Handling: try, except, else, finally
6 min read
When code breaks at runtime
Not every bug shows up before your program runs. A SyntaxError gets caught the moment Python tries to read your file — but plenty of errors only happen while the program is running: dividing by zero, opening a file that doesn't exist, looking up a dictionary key that was never set.
These runtime errors are called exceptions. An exception is an error that happens during execution. If nothing catches it, Python stops the program right there and prints a traceback:
def divide(num):
print(100 / num)
divide(0)
Output:
ZeroDivisionError: division by zero
That crash is Python doing exactly what it's supposed to: it can't continue safely, so it stops. Exception handling is how you tell Python "I know this might fail — here's what to do if it does" instead of letting the whole program die over one bad input.
Two steps: raising and catching
Exception handling has two sides:
- Raising (or throwing) — an error occurs, and Python creates an exception object describing it
- Catching — your code intercepts that exception and decides what to do next
Python gives you three keywords for this: raise, try, and except.
try / except: catching an error
Wrap the risky code in a try block. If it raises an exception, Python jumps straight to the matching except block instead of crashing:
def divide(num):
try:
print(100 / num)
except ZeroDivisionError:
print("Division by zero not allowed")
divide(0)
Output:
Division by zero not allowed
Naming ZeroDivisionError specifically means this except only catches that kind of error — a TypeError elsewhere in the try block would still crash the program. That's usually what you want: catch the failure you anticipated, and let anything unexpected surface loudly rather than hiding it.
How the whole flow fits together
Diagram — How try / except / else / finally Flow
Python always runs try first. From there, exactly one of except or else runs, and finally runs no matter which path was taken.
try / except / else
Add an else block to run code only when the try block succeeded — no exception at all:
def divide(num):
try:
result = 100 / num
except ZeroDivisionError:
print("Division by zero not allowed")
else:
print(f"Result is {result}")
divide(10)
Output:
Result is 10.0
Why not just put that print() at the end of the try block instead? Because else only runs if no exception occurred anywhere in try — it keeps "the code that might fail" and "the code that only makes sense if it didn't" visually separate, which matters once the try block does more than one thing.
try / except / finally
A finally block runs always — whether the try succeeded, failed, or even if you return out of the middle of it. It's the right place for cleanup that has to happen either way, like closing a file or releasing a resource:
def divide(num):
try:
result = 100 / num
except ZeroDivisionError:
print("Division by zero not allowed")
finally:
print(f"Input was {num}")
divide(0)
Output:
Division by zero not allowed
Input was 0
try, except, else, and finally can all appear together in one block — else for the success-only path, finally for the no-matter-what path.
Common built-in exceptions
Python raises specific exception types depending on what went wrong, so your except clause can react precisely instead of guessing:
| Exception | Raised when |
|---|---|
ZeroDivisionError | Dividing (or taking modulo) by zero |
FileNotFoundError | The file you tried to open doesn't exist (a subtype of OSError) |
ImportError | Python can't find the module you tried to import |
ValueError | An operation gets an argument of the right type but an invalid value (e.g. int("abc")) |
KeyError | A dictionary lookup uses a key that doesn't exist |
IndexError | A list or tuple is indexed out of range |
TypeError | An operation is applied to a value of the wrong type |
IndentationError | Code is indented incorrectly |
SyntaxError | Python's parser can't understand the code at all |
You can catch more than one type at once with a tuple: except (KeyError, IndexError): — or catch anything at all with a bare except Exception:, though it's best reserved for cases where you truly don't care why it failed, only that it did.
Raising your own exceptions
Use raise to trigger an exception on purpose — useful when your own code detects a problem that Python's built-in exceptions don't quite describe:
class MyException(Exception):
pass
def divide(num):
try:
return 100 / num
except ZeroDivisionError:
raise MyException("Cannot divide by 0")
divide(0)
Output:
__main__.MyException: Cannot divide by 0
A custom exception is just a class that inherits from Exception — often with nothing else in its body (pass is enough). Defining your own exception types lets callers of your code catch your specific error (except MyException:) instead of a generic one, which makes larger programs much easier to debug.
Key takeaway
An exception is a runtime error that crashes your program unless something catches it. try wraps risky code; except catches a specific exception type and handles it; else runs only if nothing went wrong; finally always runs, making it the place for cleanup. You can also raise your own exceptions — including custom ones defined as subclasses of Exception — when your code detects a problem Python doesn't already have a name for.
What's next?
You've now covered every major error-handling tool Python gives you. It's worth revisiting Modules, Libraries, and File I/O with this lesson in mind — file operations are one of the most common places exceptions come up in real programs, since a file might not exist, might be in the wrong format, or might be locked by another process. Wrapping open() calls in a try/except is standard practice.
From here, the course moves into two more advanced techniques: Lambda Functions, Python's syntax for small, anonymous functions, and then classes and objects — how to define your own data types.