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.
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.
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.
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.
You write my_dict["username"] and the key "username" does not exist in the dictionary. Which exception does Python raise?
Build the line of code that would raise a ZeroDivisionError:
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.
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.
What does this program print? Think through it before choosing.
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.
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.
try:
value = int(input("Enter a number: "))
except ValueError as e:
print(f"Input error: {e}")
This function is supposed to safely divide two numbers. One line contains a bug. Click it, then hit check.
accept to except on line 4. The keyword that catches exceptions is except — accept 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.
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().
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.")
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.
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.
This function should print a success message only when no exception occurred, then always print "done". One line is wrong. Find it.
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.
# 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 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 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
Given this code and this scenario, which blocks execute? Select all that apply, then hit check.
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 (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.
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 bysys.exit()),GeneratorExit, andException(everything else).- Should you catch it?
- Almost never. Catching
BaseExceptionswallowsKeyboardInterruptandSystemExit, 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
Exceptionbroadly 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 aValueErroror 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.
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.
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.
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
KeyErroron a dictionary you control, or anAttributeErroron 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/printadds noise without value. Let the exception terminate the program with a proper traceback. - When the exception is too broad for the context. Catching
Exceptionaround a ten-line block that does five different things means any one of those five could fail invisibly. Keeptryblocks 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.
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.
What does this program print? Trace it mentally before choosing.
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:
# 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.
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.
# 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.
# 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.
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."
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.
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.
-
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
tryblock. Keep thetryblock as narrow as possible so you know exactly which operation raised the exception. -
Write an except clause for the expected exception
Immediately after the
tryblock, write anexceptclause naming the specific exception type you expect, such asValueErrororZeroDivisionError. Add the code you want to run when that error occurs. Avoid catchingExceptionor using a bareexceptunless you have a very specific reason. -
Add else for success-only logic
If you have code that should only run when no exception was raised, place it in an
elseblock after theexceptclause. This keeps your success-path logic clearly separate from your error-handling logic and makes the function easier to read and test. -
Use finally for cleanup
Add a
finallyblock for any code that must always run, such as closing a file handle or releasing a resource. Thefinallyblock executes whether or not an exception occurred, so you can rely on it to tidy up regardless of the outcome.
Summary
- 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.
- The
tryblock holds risky code. Theexceptblock handles a specific exception type. Theelseblock runs only on success. Thefinallyblock runs always — making it the right place for cleanup. - Use specific exception types in your
exceptclauses rather than catching everything. This keeps error handling predictable and avoids masking unrelated bugs in your program. - Read tracebacks from the bottom up. The last line names the exception type and message; the last frame above it is the failure site.
BaseExceptionis the root of all exceptions.Exceptioncovers all ordinary runtime errors and is the broadest type you should routinely catch. A bareexceptcatchesKeyboardInterruptandSystemExit— almost always the wrong choice.- The exception object bound with
as eexposesstr(e),repr(e),type(e).__name__, ande.args— useful for logging, APIs, and structured error responses. - A bare
raiseinside anexceptblock re-raises the current exception unchanged, letting it propagate after you have logged or observed it. - 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.
- Where you place
try/exceptrelative to a loop determines whether one failure stops all processing (try outside) or allows each iteration to recover independently (try inside). - Custom exceptions inherit from
Exceptionand give your domain-specific errors a precise name and structured data, making large codebases far easier to maintain. - Python favors EAFP (Easier to Ask Forgiveness than Permission) over LBYL (Look Before You Leap). Attempt the operation directly inside a
tryblock rather than testing for preconditions — EAFP is shorter, more idiomatic, and immune to race conditions between the check and the act. contextlib.suppress(ExceptionType)is the clean, readable alternative totry/except: passwhen you genuinely intend to silence a specific exception.sys.exc_info()returns the active exception's type, value, and traceback from inside anexceptblock — 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
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
# 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
# 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.
What does this program print? This one tests else and finally together.
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.
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, andfinally. - 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
finallywhen abreak,continue, orreturnappears 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 theexcept*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 theraise ... fromsyntax 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*, andadd_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.