7 Exceptions and Error Handling

A practical progression through Python exceptions: recognizing runtime failures, handling expected problems, validating inputs, managing resources, and designing informative error messages.

Recognizing Python failures

Python distinguishes failures that prevent parsing from failures found during execution. A syntax error prevents the program from being parsed, whereas an exception occurs while syntactically valid code is running. If no handler deals with an exception, the current operation stops and Python displays a .

The final line of a identifies the and message. The lines above it show the call path that led to the failure, so reading from the bottom upward gives a quick route from symptom to location.

Common runtime failures include:

  • ValueError: a value has the right general type but invalid content, as in int("abc").

  • TypeError: an operation receives an inappropriate type, as in "total" + 3.

  • NameError: a variable or name has not been defined.

  • ZeroDivisionError: division or modulo uses zero.

  • IndexError: a sequence index is outside its valid range.

  • KeyError: a requested dictionary key does not exist.

  • FileNotFoundError: a requested file or directory does not exist.

  • PermissionError: the operating system does not permit the operation.

  • AttributeError: an object lacks the requested attribute.

  • AssertionError: an assert condition is false.

An belongs to an inheritance hierarchy. For example, FileNotFoundError is a specialized form of OSError, and application-specific exceptions should normally inherit from Exception rather than directly from BaseException.

Takeaway: Identify the and read the before deciding how to respond.

Handling expected exceptions

Use a when a particular operation may fail and the program has a meaningful response to that failure. The try block contains the risky operation, and a matching except handler runs only when that operation raises the selected exception.

A simple input-conversion pattern is: place age = int(input("Enter your age: ")) in the try block; use except ValueError: to print Please enter a whole number.; and use else: to print the successfully converted age. If conversion fails, the remaining statements in the try block are skipped and the ValueError handler runs. If conversion succeeds, the handler is skipped and the else block runs.

Multiple handlers are useful when different failures require different responses. For example, a file-reading operation can use except FileNotFoundError: to report that settings.txt was not found and except PermissionError: to report that the operating system denied access.

Prefer specific handlers. A broad except Exception: can be reasonable at a program boundary where an unexpected failure must be logged, but using it throughout ordinary logic can hide programming errors. Avoid a bare except: unless handling exceptions derived from BaseException, such as KeyboardInterrupt or SystemExit, is intentional.

Takeaway: Catch only failures that the current part of the program can handle meaningfully, and keep each protected block small.

Separating success and cleanup

The optional clauses after a try block have distinct purposes:

  • else runs only when the try block finishes without raising an exception. Put success-dependent work there when you do not want it accidentally caught by the preceding handlers.

  • finally runs whether an exception occurs or not. Use it for cleanup, such as releasing a resource or restoring program state.

A typical structure performs an operation in try, reports a SpecificError in except, uses the result in else, and calls a cleanup operation in finally. The clauses must appear in the order try, optional except handlers, optional else, and optional finally.

A finally block also executes when the try block returns. However, placing return, break, or continue inside finally can suppress a pending exception or override an earlier return value, so this pattern should generally be avoided.

For files and other resources that support the context-manager protocol, prefer a with statement. For example, with open("notes.txt", encoding="utf-8") as file: followed by contents = file.read() allows the resource to be cleaned up automatically. The performs normal cleanup even when an exception occurs inside the block.

Takeaway: Use else for successful follow-up work, finally for unavoidable cleanup, and a when a resource supports it.

Raising and validating

Use raise when a function cannot fulfill its contract or when an input violates a required rule. It may receive an exception class or an exception instance. For example, a set_percentage function can check whether 0 <= value <= 100, raise ValueError("percentage must be between 0 and 100") when the check fails, and return the value otherwise.

should follow a deliberate sequence:

  1. Identify the contract: decide which values are acceptable.

  2. Check the boundary: validate data as it enters a function or component.

  3. Raise a suitable exception: use TypeError for an inappropriate type and ValueError for an invalid value.

  4. State the requirement clearly, including the received value when it is safe and useful.

  5. Reject invalid data before it spreads to code that would make unsafe assumptions.

For example, a create_user function can use isinstance(username, str) to reject a non-string username with TypeError, reject an empty or whitespace-only username with ValueError, use isinstance(age, int) to reject an inappropriate age type with TypeError, and reject a negative age with ValueError. After these checks, it can return a dictionary containing the cleaned username and age.

For interactive input, catch an expected conversion error and repeat the prompt rather than terminating the program. A loop can attempt to convert a quantity with int, reject a quantity below the required minimum by raising ValueError, report the reason, and stop only after valid input is received.

Inside an except block, a bare raise re-raises the current exception while preserving its . To translate a low-level failure into a clearer application-level error, use with raise ValueError(...) from error. The from error clause preserves the original cause for debugging while presenting a meaningful error at the function's interface.

Takeaway: Validate at boundaries and use exception types to communicate whether the type or the value is wrong.

Designing reliable error handling

Useful error messages explain three things: what failed, why it failed, and how it can be corrected. For example, bad input gives little guidance, while start date must use YYYY-MM-DD format states an actionable requirement. When a value is safe to display, include it with relevant limits, such as timeout must be between 1 and 60 seconds; got ....

Do not expose secrets, passwords, access tokens, or sensitive personal information in an error message. Preserve an original exception when it adds valuable diagnostic information, but translate low-level implementation details when callers need a simpler domain-specific explanation.

A robust design follows these principles:

  • Catch an exception only when the program can recover, retry, substitute a result, report a useful message, or add meaningful context.

  • Keep try blocks small so the protected operation is clear.

  • Order handlers from more specific exception types to more general types.

  • Do not use exceptions as a substitute for ordinary control flow when a simple condition is clearer.

  • Define custom exception classes when callers must distinguish application-specific failures.

  • Log unexpected exceptions at an appropriate boundary instead of silently ignoring them.

  • Use a for resource management rather than duplicating cleanup logic.

Takeaway: Good error handling is selective and informative: it anticipates expected failures while keeping unexpected programming errors visible during debugging.