8 Exceptions and Error Handling
A practical guide to recognizing Python failures, handling expected exceptions, propagating useful diagnostic information, and designing robust error behavior.
Recognizing Python failures
Python programs can fail in two fundamentally different ways. A occurs before execution because Python cannot parse the code; for example, omitting the colon after an if condition causes a SyntaxError. An occurs during execution even though the code was parsed successfully.
Typical runtime causes include converting unsuitable text with int, dividing by zero, using an index outside a sequence, looking up a missing dictionary key, opening a missing file, or performing an operation with an incompatible type. If no code handles the , Python displays a and stops the current operation.
The key distinction is simple: syntax errors must be fixed before the program can run, while exceptions can often be anticipated and handled while the program is running.
Takeaway: First identify whether the problem prevents parsing or occurs during execution; then choose an appropriate correction or -handling strategy.
Handling expected exceptions
Place a risky operation inside a , then follow it with one or more except clauses. If the protected code completes successfully, the handlers are skipped. If an occurs, Python stops the remaining statements in the try block and searches for a matching handler.
For example, input conversion can be handled without terminating the program:
try: age = int(input("Enter your age: "))except ValueError: print("Please enter a whole number.")
A handler should usually catch the most specific expected . File access may need separate responses for a missing file and insufficient permissions. Related exceptions can be handled together when the same response is appropriate, such as except (ValueError, TypeError):.
Use as when the response needs information from the object, as in except ValueError as error:. Avoid a bare except in ordinary application code because it can hide programming errors. A broad except should be used only with a clear reason, such as controlled logging followed by re-raising.
Takeaway: Protect only operations that may reasonably fail, and handle only failures for which the program has a useful response.
Separating success and cleanup
An else clause runs only when the try block finishes without raising an . It is useful for separating successful processing from the operation that might fail. For example, a program can read a file in try, handle file-related OSError values in except, and parse the contents in else. A malformed value can then be handled by a different, more appropriate layer instead of being mistaken for a file-access failure.
A finally clause runs whether or not an occurs. Use it for cleanup that must happen in every outcome, such as closing a resource, releasing a lock, or disconnecting from a service. The usual structure is try, optional except, optional else, and finally.
For files, the with statement is generally preferable because it closes the file automatically even when an interrupts the operation. Avoid placing return, break, or continue in finally; these statements can suppress an or replace a value that would otherwise be returned.
Takeaway: Use else for successful follow-up work and finally for unconditional cleanup; keep the original risky operation small.
Propagating and translating failures
Use the when a function detects a condition for which it cannot produce a valid result. In a withdrawal function, negative amounts or requests larger than the balance can raise ValueError, allowing the caller to decide whether to display a message, retry, or take another action. Raising an is often clearer than returning a special value such as None when that value could also be legitimate.
Inside an handler, a bare raise re-raises the same while preserving its . This is useful when a function logs or records local diagnostic information but cannot actually resolve the failure.
When a low-level failure must be expressed as a higher-level application error, use with raise RuntimeError("...") from error. The caller receives a meaningful application-level error, while the original operating-system or library error remains available as its cause.
Takeaway: Raise failures at the layer that detects them, re-raise unresolved failures, and preserve original causes when translating errors.
Designing robust error handling
A gives an application-specific failure a precise name. For example, InsufficientFundsError can represent a declined withdrawal without forcing callers to catch every possible ValueError. Related custom exceptions can inherit from a shared base such as PaymentError, allowing callers to handle all payment failures together or distinguish specialized cases.
Custom exceptions should normally inherit from , not directly from BaseException. The inheritance structure should reflect how callers need to respond: shared behavior belongs in a base , while distinct recoverable conditions can use specialized subclasses.
Robust programs validate data at boundaries, handle only errors they can correct, preserve consistent state, and provide messages that explain what failed and how to fix it. They avoid using exceptions for ordinary branching when a conditional expresses the choice more clearly. Unexpected failures should retain diagnostic details in controlled logs while users receive safe, understandable messages.
A good design often follows this sequence:
Validate incoming data.
Keep each risky operation narrow.
Catch specific expected failures.
Retry, request new input, supply a safe default, or translate the error when appropriate.
Let uncorrectable failures propagate with their causes intact.
Takeaway: Robust error handling is not merely suppressing tracebacks; it preserves useful information, protects program state, and gives each failure an appropriate response.