๐Ÿ Python

Python Exceptions โ€” Complete Exception Handling Guide

A complete, professional guide to Python exceptions โ€” covering the exception hierarchy, try-except-else-finally, raising and chaining exceptions, custom exception classes, context managers, and industry best practices.

๐Ÿ“…

Last Updated

March 2026

โฑ๏ธ

Read Time

26 min

๐ŸŽฏ

Level

Intermediate

What is an Exception in Python?

An exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. In Python, exceptions are objects โ€” instances of classes that inherit from the built-in BaseException class โ€” that are raised when an error condition occurs and can be caught (handled) using try and except blocks. Instead of a program crashing silently or printing a cryptic system error, Python converts every runtime problem into a structured, catchable object with a type, a message, and a traceback.

Python treats errors as first-class citizens of the language. Almost everything that can go wrong โ€” dividing by zero, accessing a missing dictionary key, opening a file that doesn't exist, calling a method on None โ€” raises an exception rather than returning an error code. This is often summarised by Python's philosophy of "It's easier to ask forgiveness than permission" (EAFP), which favours attempting an operation and handling the exception if it fails, over checking every precondition in advance ("Look Before You Leap" โ€” LBYL, more common in C-style languages).

It's important to distinguish exceptions from syntax errors. A SyntaxError is detected by the parser before your program even starts running โ€” it means the code itself is not valid Python. An exception, by contrast, occurs while a syntactically correct program is running โ€” for example 10 / 0 is perfectly valid syntax, but it raises a ZeroDivisionError the moment it executes.

When an exception is raised and nothing catches it, Python prints a traceback โ€” a report showing the exact sequence of function calls (the call stack) that led to the error, the file and line number of each call, and the final exception type and message. Reading tracebacks correctly, from the bottom up, is one of the most important debugging skills for any Python developer.

The Python Exception Hierarchy

Every exception in Python is a class, and every exception class ultimately inherits from BaseException โ€” the root of the entire exception tree. Understanding this hierarchy is essential because it determines what an except clause will and won't catch, and it explains why certain exceptions (like KeyboardInterrupt) should almost never be silently swallowed.

  • โ–ถ

    BaseException โ€” The root of all exceptions. Almost never caught directly in application code; doing so would also catch system-exiting signals, which is rarely the intent.

  • โ–ถ

    SystemExit โ€” Raised by the built-in <code>sys.exit()</code> function to terminate the interpreter. Direct child of BaseException, not Exception, so a bare <code>except Exception</code> will not accidentally catch it.

  • โ–ถ

    KeyboardInterrupt โ€” Raised when the user presses Ctrl+C. Also a direct child of BaseException for the same reason โ€” you don't want a broad except clause to block a user's shutdown request.

  • โ–ถ

    GeneratorExit โ€” Raised inside a generator or coroutine when its <code>close()</code> method is called, signalling that iteration should stop.

  • โ–ถ

    Exception โ€” The base class for essentially all errors that application code is expected to catch and handle. This is the class you should subclass when creating your own custom exceptions.

  • โ–ถ

    ArithmeticError โ€” Base class for numeric errors, with children ZeroDivisionError, OverflowError, and FloatingPointError.

  • โ–ถ

    LookupError โ€” Base class for indexing/key errors, with children IndexError (sequence index out of range) and KeyError (dictionary key not found).

  • โ–ถ

    ValueError โ€” Raised when an operation receives an argument of the right type but an inappropriate value, e.g. int('abc').

  • โ–ถ

    TypeError โ€” Raised when an operation is applied to an object of an inappropriate type, e.g. 'a' + 5.

  • โ–ถ

    OSError โ€” Base class for system-related errors such as file and network issues; FileNotFoundError, PermissionError, and ConnectionError all inherit from it.

  • โ–ถ

    AttributeError โ€” Raised when an attribute reference or assignment fails, e.g. calling a method that doesn't exist on an object.

  • โ–ถ

    ImportError / ModuleNotFoundError โ€” Raised when an import statement fails to find or load a module or a name within a module.

  • โ–ถ

    StopIteration โ€” Raised internally by iterators to signal that there are no more items; consumed automatically by for-loops.

  • โ–ถ

    RuntimeError โ€” A generic catch-all raised when an error doesn't fit any more specific category; RecursionError (stack depth exceeded) is a subclass.

The single most important rule that follows from this hierarchy: always catch the most specific exception class possible. Writing a bare except: or an overly broad except Exception: hides bugs, makes debugging harder, and โ€” in the case of a truly bare except: โ€” can accidentally swallow KeyboardInterrupt and SystemExit, making your program impossible to stop cleanly.

Common Built-in Exceptions โ€” Reference Table

Python ships with dozens of built-in exception classes covering nearly every category of runtime failure. This table lists the ones you will encounter most often, along with the situation that typically triggers each one.

ExceptionParent ClassTypical TriggerExample
ZeroDivisionErrorArithmeticErrorDividing a number by zero10 / 0
ValueErrorExceptionRight type, wrong valueint('abc')
TypeErrorExceptionOperation on incompatible types'2' + 2
IndexErrorLookupErrorSequence index out of range[1,2,3][10]
KeyErrorLookupErrorDictionary key not found{'a':1}['b']
AttributeErrorExceptionMissing attribute or methodNone.upper()
FileNotFoundErrorOSErrorOpening a non-existent fileopen('missing.txt')
PermissionErrorOSErrorInsufficient OS permissionsopen('/root/file','w')
ImportErrorExceptionModule or name import failsfrom x import y
ModuleNotFoundErrorImportErrorModule does not existimport notreal
StopIterationExceptionIterator exhaustednext(iter([]))
RecursionErrorRuntimeErrorMaximum recursion depth exceededInfinite recursive call
NotImplementedErrorRuntimeErrorAbstract method not overriddenSubclass forgets to implement
UnicodeDecodeErrorValueErrorInvalid byte sequence decodingb'\xff'.decode('utf-8')
OverflowErrorArithmeticErrorResult too large to representmath.exp(1000)
AssertionErrorExceptionassert statement failsassert 1 == 2

The try-except Block โ€” Basic Exception Handling

The fundamental tool for handling exceptions in Python is the try/except block. Code that might raise an exception is placed inside the try block; code that should run only if a specific exception occurs is placed inside the matching except block. If no exception occurs, every except block is skipped entirely.

Basic Syntax

๐Ÿ Pythonbasic_try_except.py
try:
    number = int(input("Enter a number: "))
    result = 100 / number
except ZeroDivisionError:
    print("You cannot divide by zero.")
except ValueError:
    print("That was not a valid number.")
else:
    print(f"100 divided by {number} is {result}")
finally:
    print("Execution of the try block has finished.")

Output

Enter a number: 0 You cannot divide by zero. Execution of the try block has finished.

Catching Multiple Exceptions in One Clause

When two or more exception types should trigger identical handling logic, they can be grouped into a single except clause using a tuple. This avoids duplicating the same handler code for each exception type.

๐Ÿ Pythonmulti_except.py
try:
    data = {"age": "twenty"}
    value = int(data["age"])
except (ValueError, KeyError) as error:
    print(f"Input problem: {error}")

Output

Input problem: invalid literal for int() with base 10: 'twenty'

The four clauses explained

  • โ–ถ

    try โ€” Contains the code that might raise an exception. Python executes this block first, watching for any errors.

  • โ–ถ

    except โ€” Runs only if the specified exception type (or one of its subclasses) is raised inside the try block. You can have multiple except clauses for different exception types.

  • โ–ถ

    else โ€” Runs only if the try block completed with no exception at all. Useful for code that should run only on success, keeping it separate from the risky code in try.

  • โ–ถ

    finally โ€” Always runs, whether an exception occurred or not, and even if the exception was not caught. Ideal for cleanup code like closing files or releasing locks.

How Python Exception Handling Works โ€” Flowchart

The diagram below traces exactly what the Python interpreter does when it hits a try block โ€” how it decides which except clause matches, when else runs, and why finally always executes regardless of the outcome.

โ–ถ๏ธ Enter try blockStart executing risky code
begins
โš™๏ธ Run try bodyStatement by statement
checks
โ“ Exception raised?Did an error occur?
no error
โœ… No exceptiontry block completed cleanly
then
๐ŸŸข Run else blockOnly executes on success
always
๐Ÿ”Ž Match except clauseCheck each except top to bottom
type matches
๐Ÿ› ๏ธ Matching except foundRun that handler's code
always
๐Ÿšซ No match foundException propagates upward
always
๐Ÿงน Run finally blockAlways executes โ€” cleanup code
finishes
๐Ÿ Continue / PropagateProgram continues or error bubbles up

Code Execution Flow โ€” from source to output

Key insight: the finally block always runs โ€” even if the exception is unhandled, even if there is a return statement inside try or except, and even if a new exception is raised inside the except block itself. The only way to skip finally entirely is if the interpreter process itself crashes (e.g. os._exit() or a fatal signal).

Raising Exceptions with the raise Statement

So far we've only handled exceptions raised automatically by Python. You can also raise exceptions deliberately using the raise keyword โ€” useful for enforcing business rules, validating function arguments, or signalling that a precondition wasn't met.

๐Ÿ Pythonraise_basic.py
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("Withdrawal amount must be positive.")
    if amount > balance:
        raise RuntimeError("Insufficient funds for this withdrawal.")
    return balance - amount

try:
    new_balance = withdraw(100, 250)
except ValueError as ve:
    print(f"Invalid input: {ve}")
except RuntimeError as re:
    print(f"Transaction failed: {re}")

Output

Transaction failed: Insufficient funds for this withdrawal.

Re-raising an Exception

Inside an except block, calling raise with no arguments re-raises the exception currently being handled, preserving its original traceback. This is a common pattern when you want to log an error before letting it propagate further up the call stack.

๐Ÿ Pythonreraise.py
import logging

def process_order(order_id):
    try:
        risky_database_call(order_id)
    except ConnectionError:
        logging.error(f"DB connection failed for order {order_id}")
        raise  # re-raises the original ConnectionError

Exception Chaining with raise ... from ...

When you catch one exception and raise a different, more meaningful one in response, Python lets you explicitly link the two using raise NewException(...) from original_exception. This preserves both tracebacks โ€” the new exception shows "The above exception was the direct cause of the following exception" โ€” which is invaluable for debugging layered systems.

๐Ÿ Pythonexception_chaining.py
class OrderProcessingError(Exception):
    pass

def load_order(order_id):
    try:
        return database.fetch(order_id)
    except KeyError as exc:
        raise OrderProcessingError(
            f"Order {order_id} could not be found."
        ) from exc

If you want to suppress the original context entirely โ€” for example when the underlying cause is irrelevant noise โ€” you can write raise NewException(...) from None, which hides the chained traceback from the output.

Creating Custom Exception Classes

Real-world applications almost always define their own exception classes rather than relying solely on built-ins. Custom exceptions make error handling code self-documenting โ€” catching InsufficientStockError is far clearer than catching a generic ValueError and inspecting its message string.

๐Ÿ Pythoncustom_exceptions.py
class ApplicationError(Exception):
    """Base class for all custom exceptions in this application."""
    pass

class InsufficientStockError(ApplicationError):
    """Raised when an order requests more stock than is available."""
    def __init__(self, product, requested, available):
        self.product = product
        self.requested = requested
        self.available = available
        message = (
            f"Cannot fulfil order for '{product}': "
            f"requested {requested}, only {available} in stock."
        )
        super().__init__(message)

def place_order(product, requested, available):
    if requested > available:
        raise InsufficientStockError(product, requested, available)
    return "Order placed successfully."

try:
    place_order("Wireless Mouse", 15, 4)
except InsufficientStockError as e:
    print(e)
    print(f"Shortfall: {e.requested - e.available} units")

Output

Cannot fulfil order for 'Wireless Mouse': requested 15, only 4 in stock. Shortfall: 11 units

Why Build an Exception Hierarchy of Your Own?

  • โ–ถ

    Single base class โ€” Defining one root exception (e.g. ApplicationError) for your whole application lets callers catch "anything my code might raise" with one except clause, without swallowing unrelated system exceptions.

  • โ–ถ

    Carries structured data โ€” Unlike a plain string message, a custom exception can carry attributes (product, requested, available in the example above) that calling code can inspect programmatically.

  • โ–ถ

    Self-documenting โ€” Exception names like PaymentDeclinedError or TokenExpiredError communicate intent instantly, both to other developers and in log files.

  • โ–ถ

    Enables precise handling โ€” Different failure modes can be handled differently by callers, rather than parsing an error message string to figure out what went wrong.

The with Statement and Context Managers

A very common source of bugs is forgetting to release a resource โ€” closing a file, releasing a lock, or closing a network connection โ€” especially when an exception causes a function to exit early. Python's with statement (backed by the context manager protocol, i.e. objects implementing __enter__ and __exit__) guarantees that cleanup happens automatically, even if an exception is raised inside the block.

๐Ÿ Pythoncontext_manager.py
# Without a context manager โ€” error-prone
file = open("data.txt", "r")
try:
    contents = file.read()
finally:
    file.close()

# With a context manager โ€” safer and shorter
with open("data.txt", "r") as file:
    contents = file.read()
# file.close() is called automatically, even if read() raises an exception

You can build your own context managers either by implementing a class with __enter__/__exit__, or more concisely using the @contextmanager decorator from the built-in contextlib module, wrapping a generator function with a single yield statement.

EAFP vs LBYL โ€” Two Philosophies of Error Handling

Python code can validate preconditions before acting (LBYL) or attempt the action and handle failure afterward (EAFP). This table compares both styles directly on the same problem.

AspectLBYL (Look Before You Leap)EAFP (Easier to Ask Forgiveness)
StyleCheck conditions first, then actAttempt the action, catch failure
Exampleif key in d: value = d[key]try: value = d[key] except KeyError: ...
Race conditionsVulnerable โ€” state can change between check and useImmune โ€” the check and use happen atomically
Performance (success path)Slightly faster with no exception overheadFast โ€” no extra check needed on the happy path
Performance (failure path)Cheap โ€” no exception machinery involvedSlower โ€” exception construction has overhead
Idiomatic inC, Go, older procedural stylesPython, and to a lesser extent Ruby
ReadabilityCan require nested conditionalsKeeps the 'happy path' code linear and clear

Python's standard library and most idiomatic Python code favour EAFP โ€” it results in code that reads cleanly along the successful path, and it avoids a whole class of race-condition bugs where a condition is checked but becomes false by the time the action is attempted.

Exception Propagation Architecture

When an exception is raised deep inside a call stack, it doesn't just vanish โ€” it propagates upward through every enclosing function until something catches it, or it reaches the top level and crashes the program. The diagram below shows this propagation path across layers of a typical application.

Application Entry Point
main()Top-level try/exceptGlobal error logger
Service Layer
OrderService.place_order()Business rule validationCustom exception raised here
Data Access Layer
Database query functionsConnection handlingOSError / ConnectionError origin
Python Interpreter (CPython)
Exception object createdTraceback frame attached per callStack unwinding begins
Unhandled Path
No matching except foundsys.excepthook invokedTraceback printed, process exits (code 1)

Architecture Diagram

Each function frame that the exception passes through, without being caught, adds one entry to the traceback โ€” this is why a traceback reads like a stack of breadcrumbs from the outermost call down to the exact line where the exception originated.

Practice: Robust File Reading with Exception Handling

Here is a realistic, production-style example that combines everything covered so far โ€” specific exception types, a custom exception, a with statement, and a finally block โ€” to read and parse a configuration file safely.

๐Ÿ Pythonsafe_config_loader.py
import json

class ConfigError(Exception):
    """Raised when the application configuration cannot be loaded."""
    pass

def load_config(path):
    try:
        with open(path, "r") as file:
            return json.load(file)
    except FileNotFoundError as exc:
        raise ConfigError(f"Config file not found: {path}") from exc
    except json.JSONDecodeError as exc:
        raise ConfigError(f"Config file is not valid JSON: {exc}") from exc
    except PermissionError as exc:
        raise ConfigError(f"No permission to read: {path}") from exc

try:
    config = load_config("settings.json")
except ConfigError as e:
    print(f"Startup aborted โ€” {e}")
else:
    print("Configuration loaded successfully.")
finally:
    print("Config loading attempt finished.")

Output

Startup aborted โ€” Config file not found: settings.json Config loading attempt finished.

Practice This Code โ€” Live Editor

Exception Handling Best Practices

Writing exception handling code that is genuinely production-ready requires more discipline than simply wrapping code in try/except. These practices separate professional error handling from code that merely appears to handle errors.

  • โ–ถ

    Catch specific exceptions, never bare except โ€” A bare except: catches everything, including KeyboardInterrupt and SystemExit, making your program impossible to interrupt cleanly and hiding real bugs.

  • โ–ถ

    Keep the try block small โ€” Only wrap the exact line(s) that can actually raise the exception you're catching. A large try block makes it unclear which statement actually failed.

  • โ–ถ

    Never use exceptions for normal control flow โ€” Exceptions should represent exceptional conditions, not everyday logic branches; overusing them for flow control hurts both performance and readability.

  • โ–ถ

    Always log before swallowing โ€” If you must catch and suppress an exception, log it first (traceback included) so the failure isn't silently lost during debugging.

  • โ–ถ

    Use finally or with for cleanup โ€” Any resource acquired (file handles, network sockets, database connections, locks) should be released in a finally block or, preferably, via a context manager.

  • โ–ถ

    Chain exceptions with raise ... from ... โ€” When translating a low-level exception into a higher-level one, always chain it so the original cause remains visible in the traceback.

  • โ–ถ

    Build a small custom exception hierarchy โ€” Define one base application exception and specific subclasses for distinct failure modes, rather than reusing generic ValueError/RuntimeError everywhere.

  • โ–ถ

    Don't return None to signal failure silently โ€” Returning None from a function that failed forces every caller to remember to check for it; raising a specific exception makes failures impossible to ignore accidentally.

  • โ–ถ

    Avoid catching exceptions you can't meaningfully handle โ€” If your except block does nothing but re-raise, consider whether that layer needs a try/except at all.

  • โ–ถ

    Test the failure paths, not just the happy path โ€” Unit tests should explicitly assert that the correct exception type is raised under bad input, using tools like pytest.raises().

Advantages and Disadvantages of Python's Exception Model

Python's exception handling design is powerful and expressive, but like any tool it has trade-offs worth understanding before leaning on it heavily.

โœ… Advantages
Clear Separation of Error Logictry/except cleanly separates the 'happy path' code from error-handling code, instead of interleaving error checks throughout normal logic.
Rich, Structured Error InformationException objects carry a type, message, and full traceback, far richer than a simple error code or boolean return value.
Encourages EAFP StyleLets code assume success and handle failure only where it actually occurs, resulting in more linear, readable logic.
Custom Exceptions Improve APIsWell-designed exception hierarchies make library and application APIs self-documenting and easy to handle correctly.
Guaranteed Cleanup via finally/withResources are reliably released even when errors occur mid-operation, preventing resource leaks.
Automatic PropagationErrors bubble up automatically through the call stack until handled, so you don't need to manually check and forward error codes at every layer.
โŒ Disadvantages
Performance Overhead on Failure PathRaising and catching exceptions is noticeably slower than a simple conditional check, which matters in tight, high-frequency loops.
Overuse Hurts ReadabilityUsing exceptions for routine control flow (instead of genuine errors) makes code harder to follow and debug.
Silent Failures with Broad except ClausesA careless except Exception: pass can hide serious bugs indefinitely, since no error is ever surfaced.
Traceback Noise in Deep Call StacksChained exceptions across many layers can produce long, hard-to-read tracebacks if not managed with raise ... from carefully.
No Compile-Time CheckingUnlike Java's checked exceptions, Python never forces callers to handle a particular exception โ€” you only discover missing handling at runtime.

Python Exceptions โ€” Interview Questions

These are the most frequently asked interview questions on Python exception handling, ranging from fundamentals to trickier edge cases interviewers use to test deeper understanding.

Practice Questions โ€” Test Your Understanding

Work through these practice questions to test your understanding of Python exception handling. Try to reason out each answer before revealing it.

1. What will this code print? try: print(1/0) except ZeroDivisionError: print('A') except Exception: print('B') finally: print('C')

Easy

2. Will 'finally' run if the try block contains a return statement?

Medium

3. What is the output of: except (TypeError, ValueError) as e: print(type(e)) when a ValueError is raised?

Easy

4. Why is catching Exception generally safer than a bare except:, but still discouraged as a habit?

Medium

5. What does 'raise' with no arguments do when used inside an except block, versus outside one?

Hard

6. What is the difference between raise ValueError('bad input') from exc and raise ValueError('bad input') from None?

Hard

7. If a custom exception class overrides __init__ but forgets to call super().__init__(message), what breaks?

Medium

8. Why does using exceptions for routine control flow hurt performance in Python compared to a simple if-check?

Hard

Conclusion โ€” Mastering Python Exception Handling

Exception handling is not an optional add-on to Python programming โ€” it is a core part of how the language expresses success and failure. Every reliable Python program, from a small script to a large-scale production service, depends on precise, intentional use of try, except, else, and finally to behave predictably when something goes wrong.

The path to mastery follows a clear progression: start by understanding the exception hierarchy so you know exactly what each except clause catches. Learn to raise exceptions deliberately to enforce your own business rules. Build a small, well-designed set of custom exception classes so failures in your application are self-documenting. Use context managers for guaranteed cleanup instead of manual finally blocks wherever possible. And above all, resist the temptation to reach for a bare except: โ€” specificity is what makes error handling code trustworthy rather than merely present.

SituationRecommended Approach
Expected, recoverable error (bad user input)โœ… Catch a specific built-in exception (ValueError, KeyError, etc.)
Domain-specific business rule violationโœ… Raise and catch a custom exception class
Resource that must always be releasedโœ… Use a context manager (with statement)
Translating a low-level error into a domain errorโœ… Use raise ... from ... to chain exceptions
Debugging an internal invariant during developmentโœ… Use assert (never for production input validation)
Silencing an error you genuinely don't care aboutโš ๏ธ Catch the specific exception, and still log it
"Just make errors go away"โŒ Never use a bare except: or except Exception: pass

The natural next step in your Python journey is to explore related topics that build directly on exception handling: context managers and the contextlib module, logging (Python's built-in logging module for structured error reporting), and unit testing failure paths using tools like pytest's pytest.raises(). Together, these form the foundation of writing Python code that fails safely, loudly, and informatively โ€” exactly when it should.

Frequently Asked Questions (FAQ)