Unit 5: Exception handling
I. Orientation — Errors and Controlled Failure
Exception handling is Python’s mechanism for detecting and responding to runtime problems without forcing a program to terminate immediately. An exception is an object representing an abnormal condition, such as invalid input, division by zero, a missing file, or an unavailable dictionary key.
- Governing principle: Separate normal program logic from error-response logic so that failures can be handled at an appropriate level.
- Exception hierarchy: Exceptions are classes arranged in an inheritance hierarchy rooted primarily at
BaseException.- Most application-level exceptions inherit from
Exception. - Examples include
ValueError,TypeError,KeyError,IndexError,FileNotFoundError, andZeroDivisionError. SystemExit,KeyboardInterrupt, andGeneratorExitinherit directly fromBaseExceptionand are generally not caught by ordinary application handlers.
- Most application-level exceptions inherit from
- Propagation: If an exception is not handled in the function where it occurs, Python passes it up through calling functions until a matching handler is found.
- Stack unwinding: During propagation, Python exits active function calls and releases their local execution contexts.
- Termination: If no matching handler exists, Python stops the program and prints a traceback showing the exception type, message, and call path.
- Core constructs: Python uses
try,except,else,finally, andraiseto control exceptional situations. - Design convention: Exceptions should represent exceptional or invalid conditions, not replace ordinary decisions that are more clearly expressed using
if, loops, or return values.
II. Catching Exceptions — Handling a Runtime Failure
Catching an exception means placing failure-prone code in a try block and defining an except block that handles a matching exception type.
A. Catching exceptions
A single exception handler allows a program to recover from one anticipated category of failure.
- Basic syntax: Python executes the
trysuite first and enters theexceptsuite only if the specified exception occurs.
try:
result = 10 / divisor
except ZeroDivisionError:
result = Nonedivisoris the number by which10is divided.resultreceives the quotient when division succeeds andNonewhendivisoris zero.ZeroDivisionErroris the exception class matched by the handler.
- Capturing the object: The
asclause binds the exception object to a name, making its message available.
try:
age = int(user_input)
except ValueError as error:
print(f"Invalid age: {error}")user_inputis the text being converted.agestores the resulting integer.errorrefers to the caughtValueErrorobject.
- Matching rule: An
exceptclause matches both the named class and its subclasses. For example,except LookupErrorcatches bothKeyErrorandIndexError. - Protected region: Only statements that may produce the anticipated exception should normally be placed in
try; a narrow block makes the source of failure clear. elseclause: Anelsesuite runs only when thetrysuite finishes without an exception.
try:
value = int(text)
except ValueError:
print("Not an integer")
else:
print(value * 2)finallyclause: Afinallysuite runs whether execution succeeds, an exception is caught, or an exception continues propagating. It is suitable for mandatory cleanup such as releasing a lock.- Bare handler risk:
except:catches almost every exception derived fromBaseException, including interruption and exit signals. Prefer a specific class or, when a broad application boundary is necessary,except Exception. - Scope limitation: A handler cannot repair corrupted state automatically; it must perform a meaningful action such as supplying a fallback, logging the failure, retrying safely, or allowing propagation.
B. Applications and limitations
Effective catching preserves useful behavior while avoiding the concealment of programming defects.
- Appropriate recovery: Catch
FileNotFoundErrorwhen a missing optional configuration file has a valid default. - Inappropriate suppression: An empty handler such as
except ValueError: passmay hide malformed data and make later failures harder to diagnose. - Resource safety: Context managers are often clearer than manual cleanup.
with open("data.txt") as file:closes the file even when processing raises an exception. - Traceback preservation: If the current layer cannot recover meaningfully, it should let the exception propagate rather than replacing it with an unrelated fallback.
III. Catching Multiple Exceptions — Distinguishing Failure Categories
A try statement may handle several exception types, either with separate responses or with one shared response.
A. Catching multiple exceptions
Multiple handlers allow error responses to reflect the precise kind of failure that occurred.
- Separate handlers: Use several
exceptclauses when exception types require different actions.
try:
item = records[index]
number = int(item)
except IndexError:
print("The index is outside the list")
except ValueError:
print("The selected item is not an integer")- Concrete distinction:
IndexErrormeansrecords[index]failed, whereasValueErrormeansint(item)received unsuitable text. - Single selection: At most one matching
exceptsuite runs for a particular exception. - Ordering: Handlers are tested from top to bottom, so subclasses must appear before parent classes.
try:
process()
except FileNotFoundError:
print("Required file is missing")
except OSError:
print("Another operating-system error occurred")FileNotFoundErroris a subclass ofOSError; reversing these clauses would make the specific handler unreachable.
- Grouped handler: Use a tuple when several exceptions require exactly the same response.
try:
value = numbers[position] / divisor
except (IndexError, ZeroDivisionError) as error:
print(f"Calculation failed: {error}")-
Tuple meaning: The handler runs if either
IndexErrororZeroDivisionErroroccurs. -
Shared object:
errorrefers to whichever exception instance was raised. -
Trade-off: Grouping reduces repetition but is unsuitable when recovery depends on the particular failure.
-
Broad final handler: A final
except Exception as errormay log unexpected application errors after specific handlers, but it should not silently continue when program state is uncertain. -
No-match behavior: If none of the clauses match, the exception propagates normally.
-
elseplacement: Successful operations that could themselves raise unrelated exceptions should be moved intoelse, preventing an earlier handler from catching them accidentally.
B. Selection guidelines
Handler structure should communicate whether failures are equivalent or require distinct recovery.
- Use separate clauses: Choose them when missing data, invalid data, and unavailable resources lead to different messages or fallback actions.
- Use a tuple: Choose it when the same cleanup, conversion to a status value, or user-facing response applies to every listed type.
- Avoid oversized tuples: Grouping unrelated exceptions may erase information needed for diagnosis.
- Catch at the right level: Low-level code often lacks enough context to recover, while a service boundary may know whether to retry, reject input, or report failure.
IV. Raising Exceptions — Signalling Invalid Conditions
Raising an exception deliberately interrupts normal execution when code detects a condition that violates its requirements.
A. Raising exceptions
The raise statement creates or propagates an exception so that a suitable caller can handle it.
- Explicit raising: Supply an exception instance containing a clear diagnostic message.
def set_percentage(value):
if not 0 <= value <= 100:
raise ValueError("percentage must be between 0 and 100")
return valuevalueis the proposed percentage.- The condition defines the valid inclusive interval
[0, 100]. ValueErrorindicates that the argument has an acceptable general type but an invalid value.
- Class form:
raise ValueErroris legal because Python instantiates the class, butraise ValueError("message")communicates the violated rule more clearly. - Type choice: Use established built-in exceptions when their meanings fit.
TypeError: An argument has an inappropriate type.ValueError: Its type is acceptable, but its value is invalid.RuntimeError: A runtime condition has no more specific standard category.NotImplementedError: A required operation is intentionally unsupported by a base implementation.
- Re-raising: A bare
raiseinside anexceptsuite propagates the currently handled exception while preserving its traceback.
try:
save_record(record)
except OSError:
log_failure(record)
raise- Exception chaining:
raise NewError(...) from original_errorpresents a higher-level meaning while preserving the direct cause.
try:
port = int(text)
except ValueError as error:
raise ConfigurationError("port must be an integer") from errortextis the configuration value.portis the intended integer.erroris the original conversion failure.ConfigurationErrorexpresses the application-level problem.
- Implicit context: Raising a new exception during handling automatically records the earlier exception as context;
frommakes the causal relationship explicit. - Control-flow effect: Statements following
raisein the same execution path do not run unless the exception is caught and control later returns through another path.
B. Applications and limitations
Deliberate raising enforces contracts close to the point where a violation becomes known.
- Input validation: Reject a negative quantity before it enters inventory calculations.
- State validation: Raise an exception if an operation requires an authenticated session but none exists.
- Message quality: State the failed requirement, such as
"timeout must be positive", rather than a vague message such as"bad input". - Limitation: Do not catch and immediately re-raise without logging, translation, cleanup, or another concrete purpose; the extra handler adds noise.
V. Custom Exception — Representing Domain-Specific Failure
A custom exception is a user-defined class that gives application-specific failures a stable, meaningful identity.
A. Custom exception
Custom exceptions should normally inherit directly or indirectly from Exception, allowing ordinary application handlers to catch them safely.
- Minimal definition: An empty subclass is sufficient when the class name carries the meaning.
class InsufficientFundsError(Exception):
"""Raised when an account cannot cover a withdrawal."""- Usage: Raise the custom type at the point where the domain rule is violated.
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(
f"requested {amount}, available {balance}"
)
return balance - amountbalanceis the available amount.amountis the requested withdrawal.- The exception identifies failure of the account-balance rule.
- Structured data: A custom constructor can retain values for programmatic inspection.
class InsufficientFundsError(Exception):
def __init__(self, requested, available):
self.requested = requested
self.available = available
super().__init__(
f"requested {requested}, available {available}"
)requestedandavailablebecome attributes on the exception object.super().__init__(...)initializes the standard exception message inException.args.
- Exception family: Related errors can inherit from one application base class.
class PaymentError(Exception):
pass
class InsufficientFundsError(PaymentError):
pass
class PaymentDeclinedError(PaymentError):
pass- Flexible handling: Callers may catch
InsufficientFundsErrorspecifically or catchPaymentErrorfor all payment-domain failures. - Naming convention: Exception class names conventionally end in
Error, clearly identifying their purpose. - Inheritance restriction: Avoid inheriting directly from
BaseException; doing so may bypass ordinaryexcept Exceptionapplication boundaries.
B. Applications and limitations
Custom types are most useful when callers need to distinguish a domain failure from generic implementation errors.
- API clarity:
ConfigurationErrorcommunicates more intent than exposing an internalKeyError. - Stable boundaries: A library can translate database or network exceptions into documented domain exceptions while retaining the cause with
raise ... from .... - Useful attributes: Fields such as
requested,available, orresource_idenable handlers to respond without parsing message text. - Avoid unnecessary classes: If
ValueErrorprecisely describes a small function’s invalid argument, a custom type may add complexity without improving handling. - Responsibility boundary: A custom exception identifies failure but does not determine recovery; the catching layer must still choose whether to retry, report, compensate, or terminate.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →