Learn What Exception Handling is in Python: Absolute Beginners Tutorial

Exception handling is how Python programs respond to runtime errors without crashing. When something unexpected happens — a user enters letters where a number was expected, a file is missing, or a calculation hits a divide-by-zero — Python raises an exception. This tutorial covers what that means, how try and except work, and how to write code that handles errors deliberately. It is part of the python tutorials series on PythonCodeCrack, which covers Python from first principles through advanced patterns.

Every Python program can run into unexpected situations. A user might type something invalid, a file might not exist, or a calculation might be mathematically impossible. Without any handling in place, Python stops the program and prints an error message. Exception handling gives you a way to anticipate these situations and decide what should happen instead of a crash.

mental model

Think of exception handling as a safety net under a tightrope walker. The tightrope is your normal code path — the walker moves forward assuming everything goes right. The net is your try/except block. When the walker falls (an exception is raised), the net catches them (the except block runs) and the performance continues rather than ending in disaster.

The else block is the applause that happens only if the walker makes it across without falling. The finally block is the stage crew that strikes the equipment no matter what — whether the walk succeeded, failed, or was cut short. And raise is the walker choosing to step off deliberately — signalling a problem on purpose.

Hold this picture in mind as you read. Every piece of syntax maps to a role in the same story.

What Is an Exception?

An exception is a signal that something went wrong during the execution of a program. Python's official documentation describes exceptions as events that disrupt the normal flow of a program's instructions. They are different from syntax errors, which prevent a program from running at all. Exceptions happen while the program is running.

When Python encounters a problem it cannot resolve — such as dividing a number by zero — it raises an exception. Raising means Python creates an exception object that describes the problem and then looks for handling code. If none is found, the program terminates and prints a traceback.

Note

A syntax error is caught before the program runs — Python refuses to execute code that cannot be parsed. An exception is caught at runtime — the code looked fine to the parser but something went wrong during execution. Only exceptions can be handled with try and except.

Python has many built-in exception types, each representing a different category of problem. Knowing the common ones helps you write more targeted handling code.

A function receives the right type but an inappropriate value.
An operation is applied to an object of the wrong type.
Division or modulo by zero.
A sequence subscript is out of range.
A dictionary key does not exist.
A file or directory cannot be found.
An attribute reference or assignment fails.
A local or global name is not found — referencing a variable before it has been defined.

When any of these conditions arise, Python raises the corresponding exception. The program does not have to stop — that is what exception handling is for.

quick check

You write my_dict["username"] and the key "username" does not exist in the dictionary. Which exception does Python raise?

code builder click a token to place it

Build the line of code that would raise a ZeroDivisionError:

your code will appear here...
0 print result / = 10 *
Why: result = 10 / 0 assigns the result of dividing 10 by zero to result. Division by zero is mathematically undefined, so Python raises a ZeroDivisionError at this line. The distractor tokens print and * do not belong here.

The try and except Blocks

The try block marks code that might raise an exception. The except block defines what should happen if it does. Python executes the try block line by line. If a line raises an exception that matches the except clause, Python stops executing the try block, jumps to the except block, and runs it. The program then continues normally after the whole try/except structure.

python
user_input = "forty-two"

try:
    number = int(user_input)  # This raises ValueError
    print("You entered:", number)
except ValueError:
    print("That is not a valid number.")

print("Program continues here.")

In this example, int("forty-two") raises a ValueError because the string cannot be converted to an integer. The except ValueError clause catches that specific exception and prints a message. The final print runs normally because the exception was handled — the program did not crash.

predict the output read, think, then click your answer

What does this program print? Think through it before choosing.

x = "99" try: result = int(x) print("converted:", result) except ValueError: print("not a number") print("done")
Tip

Always name the specific exception type in your except clause rather than using a bare except. Bare except clauses catch everything — including keyboard interrupts and system exits — which makes bugs harder to find and programs impossible to stop with Ctrl+C.

You can catch multiple exception types in a single except clause by grouping them in a tuple, or you can stack multiple except clauses to handle different exceptions in different ways. Pythonic practice is to keep exception handler bodies free of side effects like printing — let the caller decide how to present errors. Functions should either return a sentinel value like None, or raise a more specific exception that the caller can handle on its own terms.

python
def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        return None  # caller handles the messaging
    except TypeError:
        return None
    return result

print(divide(10, 2))   # 5.0
print(divide(10, 0))   # None
print(divide(10, "x")) # None

# If you need callers to know why None was returned, raise instead:
def divide_strict(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        raise ValueError(f"Cannot divide {a} by zero") from e
    except TypeError as e:
        raise TypeError(f"Both arguments must be numbers, got {type(a).__name__} and {type(b).__name__}") from e

You can also capture the exception object itself using the as keyword. This gives you access to the error message Python generated, which is useful for logging or displaying a more detailed response.

python
try:
    value = int(input("Enter a number: "))
except ValueError as e:
    print(f"Input error: {e}")
spot the bug click the line that contains the bug

This function is supposed to safely divide two numbers. One line contains a bug. Click it, then hit check.

1 def safe_divide(a, b):
2 try:
3 return a / b
4 accept ZeroDivisionError:
5 return None
The fix: change accept to except on line 4. The keyword that catches exceptions is exceptaccept is not a Python keyword and would cause a SyntaxError. The division on line 3 is correct.

else, finally, and raise

Beyond try and except, Python gives you two additional clauses — else and finally — that let you organize your handling logic more precisely. You can also raise exceptions yourself using the raise keyword.

The else clause

The else block runs only if the try block completed without raising any exception. This is useful when you have code that should only execute on success, and you want to keep it separate from both the risky code in try and the error handling in except.

python
try:
    number = int("42")
except ValueError:
    print("Could not convert to integer.")
else:
    print("Conversion succeeded:", number)
    # This only runs if no exception was raised

The finally clause

The finally block runs no matter what — whether an exception was raised, caught, or never occurred. It is the right place for cleanup code that must always execute, such as closing a file or releasing a network connection. Note that open() must be inside the outer try block so that a FileNotFoundError is caught before the inner finally block tries to call file.close().

python
try:
    file = open("data.txt", "r")
    try:
        contents = file.read()
    finally:
        file.close()  # Always runs — file is always closed
except FileNotFoundError:
    print("File not found.")
Note

In practice, use a with statement when opening files. It handles closing automatically — even if an exception occurs — and is cleaner than a nested try/finally: with open("data.txt") as f: contents = f.read().

The raise statement

You can raise exceptions yourself when your code encounters a condition that is logically invalid. This is how you enforce rules in your own functions. Use raise followed by an exception type and a message string describing the problem.

python
def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative.")
    if age > 150:
        raise ValueError("Age value is unrealistically large.")
    return age

try:
    print(set_age(-5))
except ValueError as e:
    print(f"Invalid age: {e}")

The accordion below compares the four clauses — try, except, else, and finally — showing when each one runs and what it is used for.

spot the bug click the line that contains the bug

This function should print a success message only when no exception occurred, then always print "done". One line is wrong. Find it.

1 def read_value(data):
2 try:
3 result = int(data)
4 except ValueError:
5 print("invalid input")
6 finally:
7 print("success:", result)
8 print("done")
The fix: change finally: on line 6 to else:. The finally block runs always — including when a ValueError was raised and result was never assigned. That causes an UnboundLocalError when line 7 tries to print result. The else block is the correct clause here because it runs only when no exception occurred — exactly when result is guaranteed to exist.

Exception Chaining and Context

One detail that separates careful Python from careless Python is understanding how exceptions relate to one another when they occur in sequence. Python tracks this automatically through two mechanisms: implicit exception context and explicit exception chaining.

When an exception is raised inside an except block, Python automatically records the original exception as the context of the new one. The traceback you see will include a line reading During handling of the above exception, another exception occurred. This is implicit chaining — Python did it without being asked.

Explicit chaining uses raise ... from ... syntax. This lets you declare that a new exception is a direct consequence of a previous one, which produces the message The above exception was the direct cause of the following exception. The distinction matters when building libraries: explicit chaining communicates intent to callers who read tracebacks.

python
# Explicit exception chaining
class ConfigError(Exception):
    pass

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        # Raise a domain-specific error, preserving the original cause
        raise ConfigError(f"Config file missing: {path}") from e

try:
    load_config("settings.cfg")
except ConfigError as e:
    print(e)          # Config file missing: settings.cfg
    print(e.__cause__) # [Errno 2] No such file or directory: 'settings.cfg'

To suppress chaining entirely — for instance when you have handled the original error and the new one is unrelated — use raise NewException() from None. This sets __suppress_context__ to True on the new exception, which instructs Python's traceback formatter to omit the original from the output. The original exception is still stored in __context__ and accessible to a debugger, but it will not appear in the default traceback displayed to users.

Python 3.11+: ExceptionGroup and add_note()

Python 3.11 introduced two related features for richer exception reporting. ExceptionGroup (specified in PEP 654, authored by Irit Katriel, Yury Selivanov, and Guido van Rossum) wraps multiple simultaneous exceptions into a single object — a pattern that arises in concurrent and async code where several tasks can fail independently. The companion except* syntax lets you handle each exception type within the group independently. The standard library also defines BaseExceptionGroup for groups that may contain system-level exceptions like KeyboardInterrupt. Neither is available in Python 3.10 or earlier.

Also new in Python 3.11 is the add_note() method on all exceptions (PEP 678). It lets you attach context strings to an exception after it has been raised, which appear in the default traceback. This is useful when a handler knows something the original raise site did not: e.add_note(f"occurred while processing record {record_id}").

python
# Python 3.11+ only — ExceptionGroup with except*
try:
    raise ExceptionGroup("multiple errors", [
        ValueError("bad value"),
        TypeError("wrong type"),
    ])
except* ValueError as eg:
    print(f"Caught {len(eg.exceptions)} ValueError(s)")
except* TypeError as eg:
    print(f"Caught {len(eg.exceptions)} TypeError(s)")

The Python documentation notes that ExceptionGroup was added specifically to support asyncio.TaskGroup and similar concurrent APIs where several tasks can fail independently at the same time.

"Errors should never pass silently. Unless explicitly silenced." — Tim Peters, PEP 20: The Zen of Python

That principle is what exception handling operationalizes: silence errors only when you have decided, deliberately, that they do not matter. Every except clause should represent a conscious decision about what goes wrong and what should happen next.

When it runs
Always — it is the first block executed
Purpose
Wraps the code that might raise an exception. If an exception occurs inside the try block, execution jumps immediately to the matching except clause.
When it runs
Only when the try block raises a matching exception
Purpose
Defines the error-handling response. You can specify one or more exception types to catch, or group several types in a tuple. Stack multiple except clauses to handle different errors differently.
When it runs
Only when the try block completes without raising any exception
Purpose
Holds code that should only execute on a clean success path. Separating it from the try block makes it clear this code does not need exception protection itself.
When it runs
Always — regardless of whether an exception was raised or caught
Purpose
Guarantees that cleanup code runs no matter what happened. Typical uses include closing files, releasing locks, and disconnecting from external services.

Going Deeper: Patterns You Will Actually Use

trace the execution select every block that runs — in order

Given this code and this scenario, which blocks execute? Select all that apply, then hit check.

try: number = int("hello") print("converted:", number) except ValueError: print("caught ValueError") else: print("no exception") finally: print("cleanup")

Scenario: the string "hello" cannot be converted to int

Click every block that runs. Blocks can be correct or not depending on whether they execute in this scenario.

The mechanics of try, except, else, and finally are only part of the story. Four questions come up every time a developer moves from understanding the syntax to applying it in real code: how to read a traceback, what the exception hierarchy means for catching, how to define custom exceptions, and when to stay out of the way and let an exception travel up.

How to Read a Python Traceback

A traceback is Python's account of what went wrong and how it got there. Beginners often read them top-to-bottom, but the most useful information is at the bottom. The correct reading order is:

For a wider look at interpreting errors and diagnosing failures in Python programs, see the guide on how to debug Python code.

traceback
Traceback (most recent call last):        # ← always says this
  File "app.py", line 12, in process      # ← file, line number, function name
    result = int(user_input)              # ← the actual line that failed
ValueError: invalid literal for int()    # ← exception type: message

Start at the bottom line — that is the exception type and its message. Then read upward through the frames to trace the call path that led to the failure. Each frame shows a file name, line number, function name, and the line of code. The frame at the top of the list is where the call originated; the frame at the bottom (just above the exception line) is where the crash happened.

Rule of thumb

Read the last line first (the exception type and message). Then look at the last frame — the file and line number directly above it. Those two pieces answer 90% of debugging questions. Only work upward through the frames if the problem is not obvious from the crash site itself.

The Exception Hierarchy: BaseException vs Exception

All Python exceptions inherit from a common base class. Understanding this hierarchy explains why certain exceptions behave unexpectedly when caught — or escape your handler entirely.

What it is
The root of every exception in Python. All exceptions — including system-level signals — ultimately inherit from this class.
Subclasses you care about
KeyboardInterrupt (Ctrl+C), SystemExit (raised by sys.exit()), GeneratorExit, and Exception (everything else).
Should you catch it?
Almost never. Catching BaseException swallows KeyboardInterrupt and SystemExit, which prevents the program from being stopped or exited cleanly.
What it is
The base class for all non-system exceptions. ValueError, TypeError, FileNotFoundError, and every other everyday exception inherit from this class.
Should you catch it?
Only as a last resort in top-level error handlers (e.g., logging servers, CLI tools). In normal application code, catch the specific exception type you expect. Catching Exception broadly hides bugs that should surface and be fixed.
What they are
Concrete exception classes that name a specific category of problem. When you catch ValueError, Python only enters that handler when the raised exception is a ValueError or a subclass of it.
Best practice
Always prefer the most specific exception type that makes sense. The more specific the catch, the clearer the intent — and the less likely you are to accidentally suppress an unrelated error.

A bare except: clause catches at the BaseException level. except Exception: catches everything below BaseException — meaning KeyboardInterrupt and SystemExit still propagate normally. If you ever need a broad catch-all, except Exception is far safer than a bare except.

What You Can Do with the Exception Object

When you write except ValueError as e, the variable e holds the exception instance. That object carries several useful attributes that beginners often never discover because tutorials stop at printing it.

python
try:
    int("abc")
except ValueError as e:
    print(str(e))              # invalid literal for int() with base 10: 'abc'
    print(repr(e))             # ValueError("invalid literal for int() with base 10: 'abc'")
    print(type(e).__name__)    # ValueError
    print(e.args)              # ("invalid literal for int() with base 10: 'abc'",)
    print(e.args[0])           # invalid literal for int() with base 10: 'abc'

str(e) gives the human-readable message — suitable for user-facing output. repr(e) gives the full class name and arguments — better for logs. type(e).__name__ gives the exception class as a string without importing anything. e.args is a tuple of the arguments passed to the exception constructor; for exceptions raised with a single string, e.args[0] is that string.

These attributes matter when you are building logging systems, writing error responses for APIs, or passing error information to another layer of the application without leaking raw tracebacks to users.

quick check

You have except ValueError as e. Which expression gives you just the exception class name as a plain string — with no import required?

Re-raising Exceptions

Sometimes the right response to catching an exception is to log it, do some cleanup, and then let it continue propagating — as if the except block had never run. The bare raise statement with no argument does exactly this: it re-raises the current exception without creating a new one or altering the traceback.

python
import logging

def fetch_data(url):
    try:
        response = make_request(url)  # hypothetical network call
        return response.json()
    except ConnectionError:
        # logging.exception() captures the full traceback automatically
        logging.exception("Network failure fetching %s", url)
        raise  # re-raises the ConnectionError — caller decides what to do

Re-raising preserves the original traceback so the caller sees the full picture. It is the correct pattern when a function needs to observe or record an error but should not be the one to decide how to recover from it. Swallowing the exception by catching it without re-raising hides the failure from every caller above — which is usually the wrong choice unless recovery is genuinely complete.

When Not to Catch an Exception

Exception handling is a tool, not a requirement. Many beginners wrap everything in try/except defensively — and in doing so make programs harder to debug, harder to test, and harder to reason about. There are clear situations where catching is wrong:

  • When the error signals a programming mistake. A KeyError on a dictionary you control, or an AttributeError on an object you constructed, usually means the code has a bug — not that user input was bad. Catching it hides the bug. Let it surface.
  • When you have no meaningful recovery. If your handler would print an error and exit anyway, a bare try/except/print adds noise without value. Let the exception terminate the program with a proper traceback.
  • When the exception is too broad for the context. Catching Exception around a ten-line block that does five different things means any one of those five could fail invisibly. Keep try blocks narrow and exception types specific.
  • When the caller should decide. Library functions and utility methods usually should not catch exceptions — they should let the caller choose how to handle failures for their specific context. Re-raise or don't catch at all.

Exception Handling Inside Loops

Where you place the try/except structure relative to a loop changes what happens when an error occurs. This distinction is one of the places beginners produce code that looks correct but behaves unexpectedly.

python
values = ["10", "bad", "30", "also_bad", "50"]

# Pattern A — try wraps the whole loop
# First bad value stops all processing
try:
    results = []
    for v in values:
        results.append(int(v))
except ValueError:
    print("Stopped on bad value — processed so far:", results)
# Output: Stopped on bad value — processed so far: [10]

# Pattern B — try is inside the loop
# Each item handled independently — loop continues
results = []
for v in values:
    try:
        results.append(int(v))
    except ValueError:
        print(f"Skipping invalid value: {v!r}")
# Output: Skipping invalid value: 'bad'
#         Skipping invalid value: 'also_bad'
#         results = [10, 30, 50]

Pattern A stops at the first failure — useful when processing a batch where partial results are meaningless. Pattern B recovers from each failure and continues — useful for validating a list where bad entries should be skipped rather than halting everything. Neither is universally correct; the choice belongs to the requirements of the specific problem.

predict the output read, think, then click your answer

What does this program print? Trace it mentally before choosing.

items = ["5", "bad", "12"] results = [] for item in items: try: results.append(int(item)) except ValueError: results.append(-1) print(results)

Defining Custom Exceptions

Every exception shown so far has been a built-in. When you are writing a library or a larger application, built-in exceptions often carry the wrong meaning for your domain — a ValueError inside a payment processor tells the caller nothing about what specifically failed. Custom exceptions fix this.

A custom exception is a class that inherits from Exception (or a more specific built-in). At its simplest, the body can be a single pass:

python
# Minimal custom exception
class InsufficientFundsError(Exception):
    pass

# Custom exception with structured data
class PaymentError(Exception):
    def __init__(self, message, amount, currency="USD"):
        super().__init__(message)
        self.amount = amount
        self.currency = currency

# Raising and catching the custom exception
def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(
            f"Requested {amount} but only {balance} available."
        )
    return balance - amount

try:
    withdraw(50, 100)
except InsufficientFundsError as e:
    print(f"Transaction failed: {e}")
# Transaction failed: Requested 100 but only 50 available.

The structured version of PaymentError stores amount and currency as attributes. This lets handlers inspect these values and respond differently based on them — something a plain string message cannot support. Callers can still catch the parent Exception class if they do not care about the specifics, or catch PaymentError precisely if they do. This is the core value of exception hierarchies in your own code.

Naming convention

Custom exception names should end in Error by convention — InsufficientFundsError, ConfigurationError, AuthenticationError. This matches Python's own naming and signals immediately to readers that the class is an exception type, not a regular class.

EAFP vs LBYL: Python's Approach to Errors

Two competing philosophies govern how programs deal with potential failure. Python's glossary names them explicitly: EAFP (Easier to Ask Forgiveness than Permission) and LBYL (Look Before You Leap). Understanding the difference is one of the clearest markers between code that is idiomatic Python and code that merely runs in Python.

LBYL checks for a condition before attempting an operation. It is the defensive style: test first, act second.

python
# LBYL — check before acting (less idiomatic Python)
import os

def read_config(path):
    if not os.path.exists(path):  # check 1
        return None
    if not os.access(path, os.R_OK):  # check 2
        return None
    with open(path) as f:
        return f.read()
# Problem: a race condition exists between each check and the next operation

EAFP attempts the operation directly and handles the exception if something goes wrong. It is the idiomatic Python style: try, then recover.

python
# EAFP — attempt and handle failure
def read_config(path):
    try:
        with open(path) as f:
            return f.read()
    except (FileNotFoundError, PermissionError):
        return None

The EAFP version is shorter, handles more failure modes with less code, and avoids a race condition: the LBYL version checks whether the file exists, but it could be deleted or have its permissions changed in the milliseconds between the check and the open call. The EAFP version has no such window because the check and the act are the same operation.

Python's official glossary describes EAFP as "a common Python coding style" and contrasts it with LBYL as "common in many other languages such as C." The choice is not absolute — LBYL is appropriate when the check is semantically meaningful to readers and the operation is not atomic. But when in doubt, Python programmers prefer attempting and handling over testing and branching.

The race condition problem with LBYL

In concurrent programs, any LBYL check can become stale before the action executes. Another thread or process can modify the resource between the test and the act. EAFP is inherently immune to this class of bug because no gap exists between the check and the operation — they happen atomically from the perspective of exception handling.

contextlib.suppress and sys.exc_info()

Two tools from the standard library address scenarios that raw try/except handles awkwardly. They are rarely covered in introductory tutorials yet appear constantly in professional codebases.

contextlib.suppress

When you genuinely want to ignore a specific exception — and you want the code to be readable about that fact — contextlib.suppress is cleaner than a try/except: pass block. It is the standard library's explicit way of saying "this exception is expected and unimportant here."

python
import os
from contextlib import suppress

# Equivalent to: try: os.remove(path) except FileNotFoundError: pass
# — but communicates intent clearly

with suppress(FileNotFoundError):
    os.remove("temp_cache.txt")

# Multiple exception types suppressed with one context manager
with suppress(FileNotFoundError, PermissionError):
    os.remove("locked_file.txt")

The key difference between suppress and a bare except: pass is specificity and legibility. suppress(FileNotFoundError) is specific — it only silences that one exception type and lets all others propagate. It also reads as a deliberate design choice rather than a lazy catch-all. Raymond Hettinger, a CPython core developer, introduced contextlib.suppress in Python 3.4 specifically to make intentional suppression readable.

sys.exc_info()

Inside an except block, sys.exc_info() returns a three-tuple: (type, value, traceback). This is primarily useful in logging frameworks, decorators, and middleware that need to inspect or forward the exception without rebinding it to a variable name.

python
import logging
import sys
import traceback

def log_current_exception(logger):
    """Call from inside an except block to log the active exception.

    For simple cases, prefer logging.exception() which captures exc_info
    automatically. Use this helper when you need structured access to the
    type, value, and traceback as separate objects.
    """
    exc_type, exc_value, exc_tb = sys.exc_info()
    if exc_type is None:
        return  # no active exception — called outside an except block
    logger.error(
        "Exception: %s — %s",
        exc_type.__name__,
        exc_value
    )
    # format full traceback as a string for structured logging
    tb_lines = traceback.format_tb(exc_tb)
    logger.debug("Traceback:\n%s", "".join(tb_lines))
    del exc_tb  # avoid circular reference: frame → locals → exc_tb → frame

# Usage inside a handler
logger = logging.getLogger(__name__)
try:
    result = int("not_a_number")
except ValueError:
    log_current_exception(logger)
    raise

# Simpler alternative when you just need the traceback logged:
# logging.exception() records exc_info automatically
try:
    result = int("not_a_number")
except ValueError:
    logging.exception("Conversion failed")  # logs message + full traceback
    raise

Outside an except block, sys.exc_info() returns (None, None, None). In Python 3, the preferred way to access exception information is through the bound variable as e, but sys.exc_info() remains the correct tool when you are writing generic exception-handling infrastructure that cannot know the exception type in advance.

The Zen of Python's core instruction on errors — that silence should be deliberate, never accidental — is the thread connecting every tool in this section. contextlib.suppress is the explicit silence. sys.exc_info() is the structured observation before re-raising. EAFP is the courage to attempt rather than over-check. Each represents a deliberate relationship with failure rather than avoidance of it.

How to Handle Exceptions in Python

These four steps apply any time you add exception handling to a Python function or script.

  1. Identify the risky code

    Determine which lines of code could raise an exception at runtime — such as user input conversion, file access, or division operations — and place them inside a try block. Keep the try block as narrow as possible so you know exactly which operation raised the exception.

  2. Write an except clause for the expected exception

    Immediately after the try block, write an except clause naming the specific exception type you expect, such as ValueError or ZeroDivisionError. Add the code you want to run when that error occurs. Avoid catching Exception or using a bare except unless you have a very specific reason.

  3. Add else for success-only logic

    If you have code that should only run when no exception was raised, place it in an else block after the except clause. This keeps your success-path logic clearly separate from your error-handling logic and makes the function easier to read and test.

  4. Use finally for cleanup

    Add a finally block for any code that must always run, such as closing a file handle or releasing a resource. The finally block executes whether or not an exception occurred, so you can rely on it to tidy up regardless of the outcome.

check your understanding question 1 of 5

Summary

  1. An exception is a runtime error — different from a syntax error, which prevents execution before it starts. Exception handling responds to runtime errors instead of letting the program crash.
  2. The try block holds risky code. The except block handles a specific exception type. The else block runs only on success. The finally block runs always — making it the right place for cleanup.
  3. Use specific exception types in your except clauses rather than catching everything. This keeps error handling predictable and avoids masking unrelated bugs in your program.
  4. Read tracebacks from the bottom up. The last line names the exception type and message; the last frame above it is the failure site.
  5. BaseException is the root of all exceptions. Exception covers all ordinary runtime errors and is the broadest type you should routinely catch. A bare except catches KeyboardInterrupt and SystemExit — almost always the wrong choice.
  6. The exception object bound with as e exposes str(e), repr(e), type(e).__name__, and e.args — useful for logging, APIs, and structured error responses.
  7. A bare raise inside an except block re-raises the current exception unchanged, letting it propagate after you have logged or observed it.
  8. Do not catch exceptions when the error signals a programming mistake, when you have no meaningful recovery, or when the caller should decide what to do. Restraint in exception handling makes programs easier to debug.
  9. Where you place try/except relative to a loop determines whether one failure stops all processing (try outside) or allows each iteration to recover independently (try inside).
  10. Custom exceptions inherit from Exception and give your domain-specific errors a precise name and structured data, making large codebases far easier to maintain.
  11. Python favors EAFP (Easier to Ask Forgiveness than Permission) over LBYL (Look Before You Leap). Attempt the operation directly inside a try block rather than testing for preconditions — EAFP is shorter, more idiomatic, and immune to race conditions between the check and the act.
  12. contextlib.suppress(ExceptionType) is the clean, readable alternative to try/except: pass when you genuinely intend to silence a specific exception. sys.exc_info() returns the active exception's type, value, and traceback from inside an except block — useful in logging infrastructure and generic decorators.

Exception handling is one of the foundations of writing reliable Python. The combination of try, except, else, finally, and raise gives you full control over how your programs respond to unexpected input and runtime conditions — without crashing.

Common Exception Handling Mistakes (and What to Do Instead)

The following are patterns that appear frequently in beginner code. Each one compiles and runs without error — which is exactly what makes them dangerous. Recognizing them by shape is faster than debugging them after the fact.

Swallowing the exception with pass

python
import logging

# Mistake: silences every error with no record of what failed
try:
    result = process(data)
except Exception:
    pass

# Better: log the exception so failures are always visible
# logging.exception() records the message AND the full traceback automatically
try:
    result = process(data)
except Exception:
    logging.exception("process() failed")
    raise  # re-raise so callers are also informed

Catching an exception and doing nothing is the single most common way to create bugs that are invisible at the time of the failure and hours of confusion later. If silencing is genuinely correct — for instance, trying an optional operation — use a comment that says so explicitly.

Using a bare except instead of except Exception

python
# Mistake: bare except catches KeyboardInterrupt and SystemExit
try:
    long_running_operation()
except:
    print("Something went wrong.")

# Better: catch Exception to leave system signals free
try:
    long_running_operation()
except Exception as e:
    print(f"Operation failed: {e}")

A bare except: catches at the BaseException level, which includes KeyboardInterrupt. A user pressing Ctrl+C while your program is running will appear to do nothing if a bare except swallows the signal. Use except Exception as the widest practical catch-all.

Putting too much code inside the try block

python
# Mistake: which of these lines raised the ValueError?
try:
    raw = input("Enter a number: ")
    value = int(raw)
    result = compute(value)
    save(result)
    notify(result)
except ValueError:
    print("Something went wrong.")

# Better: protect only the line that can raise the specific exception
raw = input("Enter a number: ")
try:
    value = int(raw)
except ValueError:
    print(f"{raw!r} is not a valid integer.")
else:
    result = compute(value)
    save(result)
    notify(result)

A wide try block makes it impossible to know which line raised the exception without reading the traceback carefully. A narrow try block around only the risky line makes the intent obvious and the handler more precise.

Catching an exception you do not understand

If you are not sure what raises a RuntimeError in a third-party library, do not catch it. Read the library documentation first. Catching an exception type you do not understand means your handler will also run for failure modes you did not anticipate, possibly masking serious bugs. When in doubt, let the exception propagate and inspect the traceback to understand exactly what went wrong before writing any handler for it.

predict the output read, think, then click your answer

What does this program print? This one tests else and finally together.

def process(value): try: result = 10 / value except ZeroDivisionError: print("error") else: print("result:", result) finally: print("always") process(2)
check your understanding question 1 of 5

Certificate of Completion
Final Exam
Pass mark: 80% · Score 80% or higher to receive your certificate

Enter your name as you want it to appear on your certificate, then start the exam. Your name is used only to generate your certificate and is never transmitted or stored anywhere.

Question 1 of 10

Sources and References

Every technical claim in this tutorial is traceable to primary Python documentation or the relevant PEP. The following are the authoritative sources used in this article.

  • Python Software Foundation. 8. Errors and Exceptions — Python 3 Tutorial. docs.python.org/3/tutorial/errors.html. The canonical beginner-level introduction to exceptions; the source for how Python describes the purpose of try, except, else, and finally.
  • Python Software Foundation. Built-in Exceptions — Python 3 Library Reference. docs.python.org/3/library/exceptions.html. The complete specification of the exception hierarchy, including the definitions of BaseException, Exception, __cause__, __context__, and __suppress_context__.
  • Python Software Foundation. The try Statement — Python Language Reference. docs.python.org/3/reference/compound_stmts.html. The formal grammar and execution semantics for all try statement clauses, including the behavior of finally when a break, continue, or return appears inside the try block.
  • Katriel, Irit; Selivanov, Yury; van Rossum, Guido. PEP 654 – Exception Groups and except*. peps.python.org/pep-0654/. The design specification for ExceptionGroup, BaseExceptionGroup, and the except* syntax, introduced in Python 3.11.
  • Dörwald, Walter; van Rossum, Guido. PEP 3134 – Exception Chaining and Embedded Tracebacks. peps.python.org/pep-3134/. Specifies the __context__ and __cause__ attributes and the raise ... from syntax for explicit chaining.
  • Hatfield-Dodds, Zac. PEP 678 – Enriching Exceptions with Notes. peps.python.org/pep-0678/. Introduced the add_note() method on all exceptions in Python 3.11, enabling post-raise context annotation stored in __notes__.
  • Peters, Tim. PEP 20 – The Zen of Python. peps.python.org/pep-0020/. Source of the aphorism "Errors should never pass silently. Unless explicitly silenced." quoted in this article.
  • Python Software Foundation. What's New In Python 3.11. docs.python.org/3/whatsnew/3.11.html. Official changelog confirming ExceptionGroup, BaseExceptionGroup, except*, and add_note() as 3.11 additions.
  • Python Software Foundation. Python Glossary: EAFP. docs.python.org/3/glossary.html#term-EAFP. The official definition of EAFP and its contrast with LBYL, cited in the idiomatic exception handling section of this article.
  • Python Software Foundation. contextlib.suppress — Python 3 Library Reference. docs.python.org/3/library/contextlib.html#contextlib.suppress. Primary documentation for contextlib.suppress, introduced in Python 3.4.
  • Python Software Foundation. sys.exc_info() — Python 3 Library Reference. docs.python.org/3/library/sys.html#sys.exc_info. Specification of the sys.exc_info() function used in the logging infrastructure example.

Frequently Asked Questions

Exception handling in Python is a mechanism that lets you respond to runtime errors — called exceptions — instead of letting the program crash. You wrap risky code in a try block and define what should happen in an except block if something goes wrong.

Syntax errors happen before the program runs and cannot be caught with exception handling — the code simply will not execute. Exceptions are errors that occur during execution and can be caught and handled gracefully using try and except blocks.

The try block contains code that might raise an exception. Python executes this block first, and if any line inside it raises an exception, Python immediately jumps to the matching except block rather than crashing.

The else block runs only if the try block completed without raising any exception. It is useful for code that should only execute when no error occurred, keeping it separate from the error-handling logic in the except block.

The finally block always runs regardless of whether an exception was raised or caught. It is commonly used for cleanup tasks like closing a file, releasing a lock, or closing a database connection.

You raise an exception using the raise keyword followed by an exception type and an optional message. For example: raise ValueError("Age cannot be negative"). You can raise any built-in exception or a custom exception class you define yourself.

Common built-in exceptions include ValueError (wrong value type or range), TypeError (wrong data type), ZeroDivisionError (division by zero), IndexError (list index out of range), KeyError (dictionary key not found), and FileNotFoundError (file does not exist).

Yes. You can group multiple exception types in a single except clause by passing them as a tuple: except (ValueError, TypeError). This lets you run the same handling code for different kinds of errors.

If an exception is raised and no except block catches it, the program terminates and Python prints a traceback to the console showing the exception type, message, and the line where it occurred.

Bare except clauses — those with no exception type specified — catch everything, including KeyboardInterrupt and SystemExit. This can make programs impossible to stop with Ctrl+C and masks genuine bugs. As a rule, always specify the exception type you intend to catch.

Exception chaining occurs when a new exception is raised while handling another. Python records the original as context automatically (implicit chaining). You can also chain explicitly using raise NewException() from original, which stores the original in __cause__ and produces a clearer traceback message. To suppress chaining entirely, use raise NewException() from None.

Introduced in Python 3.11, ExceptionGroup wraps multiple exceptions into a single object so they can be raised together — useful in async and concurrent code where several tasks may fail simultaneously. The companion except* syntax lets you handle each exception type within the group independently. This feature is not available in Python 3.10 or earlier.

Start at the bottom line — that is the exception type and its message. Then look at the last frame directly above it to find the file name, line number, and the line that actually failed. Only work upward through the earlier frames if the crash site is not self-explanatory. The frames are ordered with the oldest call at the top and the failure point at the bottom.

BaseException is the root of the entire exception hierarchy, including system-level signals like KeyboardInterrupt and SystemExit. Exception is a subclass of BaseException that covers all ordinary runtime errors. Catching Exception leaves KeyboardInterrupt and SystemExit free to propagate — which is almost always what you want. A bare except catches at the BaseException level and prevents the program from being interrupted or exited normally.

Define a class that inherits from Exception (or a more specific built-in). The simplest form is class MyError(Exception): pass. For richer errors, override __init__ to accept additional data and call super().__init__(message). By convention, custom exception names end in Error.

A raise statement with no argument, used inside an except block, re-raises the current exception without modifying it or its traceback. This is the correct pattern when you need to log or observe an error and then let it continue propagating to callers above — rather than swallowing it silently.

A bare except: clause catches exceptions at the BaseException level, which includes KeyboardInterrupt and SystemExit. This means pressing Ctrl+C while a long-running program is running may appear to do nothing — the bare except intercepts the interrupt signal. Use except Exception as the broadest safe catch-all; it leaves KeyboardInterrupt, SystemExit, and GeneratorExit free to propagate normally.

Introduced in Python 3.11 via PEP 678, add_note() is a method available on all exception instances. It lets you attach additional context strings to an exception after it has been raised. Notes appear in the default traceback output and are stored in the __notes__ attribute as a list of strings: e.add_note(f"Failed while processing {filename}").

EAFP stands for "Easier to Ask Forgiveness than Permission" — Python's idiomatic coding style of attempting an operation directly and handling the exception if it fails, rather than testing preconditions first. The alternative is LBYL (Look Before You Leap), which checks conditions before acting. Python's official glossary describes EAFP as the common Python style. It is shorter, avoids race conditions between the check and the action, and handles more failure modes with less code.

contextlib.suppress is a context manager from the Python standard library that silences a specific exception type and lets all others propagate normally. It is the clean, readable alternative to try/except: pass when you genuinely intend to ignore an exception: with suppress(FileNotFoundError): os.remove(path). Use it when the exception is expected, unimportant in context, and you want the code to communicate that intent clearly to readers.