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

try:run the risky codeexception raisedno exceptionexcept:handle the errorelse:runs only if try succeededfinally:always runs, no matter what

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:

ExceptionRaised when
ZeroDivisionErrorDividing (or taking modulo) by zero
FileNotFoundErrorThe file you tried to open doesn't exist (a subtype of OSError)
ImportErrorPython can't find the module you tried to import
ValueErrorAn operation gets an argument of the right type but an invalid value (e.g. int("abc"))
KeyErrorA dictionary lookup uses a key that doesn't exist
IndexErrorA list or tuple is indexed out of range
TypeErrorAn operation is applied to a value of the wrong type
IndentationErrorCode is indented incorrectly
SyntaxErrorPython'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.