Python Programming from First Principles: Learn to Code, Think Computationally, and Solve Real-World Problems
PDFEPUB
BOOK 000 · CHAPTER 8 · Error Handling

Error Handling

On this pageSyntax Errors and Runtime ExceptionsThe Structure of Exception HandlingBasic SyntaxCatching Specific ExceptionsCatching Multiple Exceptions TogetherCatch-All Handlers — and Why to Be CarefulRaising ExceptionsCustom ExceptionsSummary

No program runs perfectly every time—or in every environment. Users type the wrong input, files go missing, network connections break, and code occasionally encounters situations the programmer did not foresee. Without proper safeguards, a single unexpected event can crash an entire application.

Error handling gives programs a safety net. Instead of stopping abruptly, Python allows a program to detect a problem, respond to it, and—if appropriate—continue running. This ability makes software robust, user-friendly, and ready for the messy realities of real-world data.

In this chapter, we explore Python’s tools for handling errors gracefully. We will distinguish between errors and exceptions, learn how try and except blocks work, explore more advanced patterns such as multiple exceptions and clean-up actions, and finish with custom exceptions that make large projects maintainable.

Syntax Errors and Runtime Exceptions

A syntax error means Python cannot parse the source. A runtime exception reports a problem encountered while valid code is executing. Both are represented by exception types in Python.

Run these two examples separately. The first is intentionally invalid: its missing parenthesis prevents that file or cell from starting.

print("Hello world"   # intentionally missing a closing parenthesis
Listing 393 — Syntax Errors and Runtime Exceptions

Expected exception type

SyntaxError

The exact diagnostic wording depends on the Python version. A try statement in the same malformed file cannot run. A running program can, however, catch SyntaxError raised while parsing other supplied source.

The next example is valid syntax, but its division fails when executed:

n = 10
result = n / 0
Listing 394 — Syntax Errors and Runtime Exceptions

Expected exception

ZeroDivisionError: division by zero

A try block around this division can handle the runtime exception. The following examples show how.

The Structure of Exception Handling

Now that we understand the difference between errors and exceptions, we can look at how Python allows you to respond to unexpected events. Python uses a small set of keywords—try,except,else, and finally—each playing a specific role in the control flow.

These four building blocks let you isolate risky code, catch problems, run fallback logic, and clean up resources reliably.

Basic Syntax

The following is a syntax template, not a runnable program. Replace the comments with statements and SomeException with a real exception type:

try:
    # Code that might cause an exception
except SomeException:
    # What to do if that exception happens
else:
    # Runs only if no exception occurred
finally:
    # Runs always, whether there was an exception or not

Each part has a specific purpose:

  • try wraps code that may fail.
  • except captures and handles specific exceptions.
  • else is for code that should run only when everything succeeded.
  • finally is ideal for cleanup tasks like closing files or releasing resources.

Example: Handling Division Safely

Here’s a short example showing all four blocks working together:

try:
    n = 0
    res = 100 / n
except ZeroDivisionError:
    print("You can't divide by zero!")
except ValueError:
    print("Invalid value provided.")
else:
    print("Result:", res)
finally:
    print("Execution complete.")
Listing 395 — Basic Syntax

Output

You can't divide by zero!
Execution complete.
  • The try block attempts to divide by zero.
  • ZeroDivisionError is caught in the first except, so the second one is skipped.
  • The else block is not executed because an exception occurred.
  • The finally block always runs, confirming the operation is complete.

This pattern becomes extremely helpful when reading files, processing user input, or calling external services—anywhere something might go wrong.

Catching Specific Exceptions

While it’s tempting to catch all errors at once, precise exception handling is often safer. Different issues require different responses: a missing file is not the same as invalid input, and neither should be treated like a network failure.

To make your program robust and easier to debug, you can catch specific exception types.

Example: Handling Two Different Problems

Here is a simple demonstration of how Python distinguishes the underlying issues:

try:
    x = int("hello")     # will cause ValueError
    inv = 1 / x          # would cause ZeroDivisionError if x were 0
except ValueError:
    print("That value cannot be converted to a number.")
except ZeroDivisionError:
    print("Zero has no multiplicative inverse.")
Listing 396 — Catching Specific Exceptions

Output

That value cannot be converted to a number.

Converting "hello" to an integer raises a ValueError, so the first except block handles it. If the string had been "0", the conversion would succeed but 1 / x would trigger a ZeroDivisionError, and the second handler would run instead.

By matching the exact exception type, your program remains clearer and more robust.

Catching Multiple Exceptions Together

Sometimes several different errors should lead to the same response. Instead of writing multiple except blocks with identical logic, Python allows grouping them into a single handler. This keeps the code more readable while remaining explicit about the problems you anticipate.

Example: Handling Value Errors and Type Errors Alike

The following example shows grouped exceptions during data conversion:

items = ["10", "twenty", 30]  # mixed input

try:
    total = int(items[0]) + int(items[1])
except (ValueError, TypeError) as e:
    print("Conversion error:", e)
except IndexError:
    print("List index out of range.")
Listing 397 — Catching Multiple Exceptions Together

Output

Conversion error: invalid literal for int() with base 10: 'twenty'

Here, converting "twenty" into an integer triggers a ValueError, which is caught by the grouped handler. A TypeError would be caught in the same place if incompatible types were used. An IndexError would be handled separately if the list had fewer elements.

Grouped exceptions let you cover several related scenarios without duplicating logic.

Catch-All Handlers — and Why to Be Careful

Python also allows you to catch any exception using a bare except: block, including KeyboardInterrupt and SystemExit.

While this can prevent crashes when you absolutely cannot afford failure, it also hides the exact error type, making real problems harder to diagnose. In large systems, that can cause silent failures.

Example: Overly Broad Exception Handling

The following example shows how a catch-all handler can obscure the real error:

try:
    result = "100" / 20  # TypeError: dividing string by number
except ArithmeticError:
    print("Some arithmetic problem occurred.")
except:
    print("Something went wrong!")
Listing 398 — Catch-All Handlers — and Why to Be Careful

Output

Something went wrong!

Dividing a string by a number raises a TypeError, which is not a subclass of ArithmeticError. The bare except catches it, but hides valuable information. Although catch-all blocks are sometimes necessary, they should be used sparingly and placed carefully.

Raising Exceptions

Sometimes the error is not Python’s fault—it’s the input or state your program has encountered. When you detect an invalid condition, raising an exception yourself is the cleanest way to alert callers that something is wrong. This ensures invalid states never silently enter your system.

Example: Rejecting Invalid Age Values

Here is a short validation example:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative.")
    print(f"Age set to {age}")

try:
    set_age(-5)
except ValueError as e:
    print(e)
Listing 399 — Raising Exceptions

Output

Age cannot be negative.

The function detects that the age is invalid and explicitly raises a ValueError. The caller handles it with a try–except block, keeping the program in a stable state. Raising exceptions gives your code a clear and consistent way to enforce rules.

Custom Exceptions

As your project grows, generic exception types may no longer provide enough detail. A large application benefits from having its own exception types that clearly communicate domain-specific problems: authentication failures, invalid transactions, exceeded quotas, or policy violations.

Python makes this easy—custom exceptions are created by inheriting from the built-in Exception class.

Example: Custom AgeError Class

Here is a short example demonstrating how to define and use your own exception type:

class AgeError(Exception):
    pass

def set_age(age):
    if age < 0:
        raise AgeError("Age cannot be negative.")
    print(f"Age set to {age}")

try:
    set_age(-5)
except AgeError as e:
    print(e)
Listing 400 — Custom Exceptions

Output

Age cannot be negative.

The class AgeError becomes a fully functional exception type unique to your application. When raised, it signals exactly what kind of problem has occurred.

Custom exceptions shine in larger systems, where different components may fail in different ways and need to be distinguished clearly.

Summary

This chapter introduced Python’s model for dealing with unexpected events: distinguishing errors from exceptions, using try,except,else, and finally, catching specific problems, raising your own exceptions, and defining custom ones for larger systems. With these tools, programs stay robust instead of crashing.

With error handling in place, we can now move on to testing, where we ensure code behaves correctly not just in theory—but in practice.