A Wandering Mind · Computer Science
PYTHON GUIDE

Learn · Run · Break · Fix

A learning series from A Wandering Mind

Eighteen interlinked lessons on strings, lists, files, exceptions, and dictionaries—with original explanations, runnable examples, real output, and supporting CPSC 1301K lectures.

course.py
>>> guide = {
...   "lessons": 18,
...   "modules": 5,
...   "python": "3.10+"
... }
>>> guide["goal"] = "understand it"
Module 4 · Errors · Lesson 15 of 18

Handling Exceptions in Python

Exceptions are Python objects that describe problems discovered while a program is running. Left unhandled, an exception stops the current flow and produces a traceback. With try and except, your program can respond to expected failures deliberately, while else and finally give you precise places for success-only work and cleanup.

Prerequisite: Complete Writing and Appending to Files. The file lessons have already exposed you to real exceptions such as FileNotFoundError, ValueError, and UnicodeDecodeError.

An exception interrupts the normal path

Python raises an exception when an operation cannot continue according to the language’s rules. Converting "abc" to an integer raises ValueError. Dividing by zero raises ZeroDivisionError. Opening a missing file for reading raises FileNotFoundError. These are not all the same problem, which is why Python gives them different exception types.

A traceback is useful during development because it shows where the failure occurred and which exception reached the top of the call stack. But some failures are predictable parts of normal program life. A user may type something invalid. A requested file may not exist. A network request may fail. Exception handling lets code define what should happen next.

try and except handle an expected failure

Place the operation that may fail inside a try block. Then add an except block for the specific exception you are prepared to handle. If that exception occurs, Python skips the remaining statements in the try block and runs the matching handler instead.

Example 1 · Handle an invalid integer conversionVerified
text = "not-a-number"
try:
    number = int(text)
except ValueError:
    print("That value is not an integer.")
That value is not an integer.

The conversion still fails. Exception handling does not make "not-a-number" a valid integer. What changes is the program’s response: the known ValueError is caught and replaced by a useful branch of application behavior.

Catch the exception you actually understand. A specific handler documents what failure the program expects and prevents unrelated bugs from being hidden behind an overly broad except.

Different exceptions can need different responses

A single try block can have multiple except clauses. Python checks them for a matching exception type. This is useful when several operations can fail for different reasons and each failure deserves a different message or recovery path.

Example 2 · Use specific handlersVerified
value = "0"
try:
    number = int(value)
    result = 10 / number
except ValueError:
    print("Enter a whole number.")
except ZeroDivisionError:
    print("Zero cannot be used as the divisor.")
Zero cannot be used as the divisor.

The text "0" converts successfully, so the ValueError handler is irrelevant on this run. Division then fails, and the ZeroDivisionError handler runs. If the input were "hello", the conversion would fail first and division would never be attempted.

else runs only when the try block succeeds

An optional else block runs when the try block finishes without raising an exception. It is useful for keeping success-only work outside the protected section.

This can make exception boundaries easier to reason about. Put only the operations whose exceptions you intend to handle in try; put follow-up work that depends on success in else.

Example 3 · Separate success with elseVerified
value = "5"
try:
    number = int(value)
except ValueError:
    print("Conversion failed.")
else:
    print(f"Converted value: {number}")
Converted value: 5

Because the conversion succeeds, the handler is skipped and the else block runs. If conversion had raised ValueError, the exception handler would run and the else block would be skipped.

finally runs whether the operation succeeds or fails

An optional finally block runs when control leaves the try statement, whether an exception occurred or not. That makes it useful for cleanup that must happen in either case.

You already saw this idea in Lesson 12, where finally could guarantee that an open file gets closed. For ordinary files, the with statement remains the cleaner choice, but finally is a general language feature used for many kinds of cleanup.

Example 4 · Capture the exception and always finishVerified
value = "0"
try:
    number = int(value)
    print(10 / number)
except (ValueError, ZeroDivisionError) as error:
    print(type(error).__name__)
finally:
    print("Attempt finished.")
ZeroDivisionError
Attempt finished.

The tuple in the handler means either listed exception type can be handled by the same block. The as error syntax gives the current exception object a temporary name. The finally block then runs after the handler.

Protected worktry:

Contains the operation whose expected failures you want to handle.

Failure pathexcept ValueError:

Runs only for a matching exception type.

Success pathelse:

Runs only if the try block completes without an exception.

Always-run pathfinally:

Runs as control leaves the statement, whether or not an exception occurred.

Error checking and exception handling are related, not identical

Sometimes a program can check a condition before attempting an operation. Before dividing, you can test whether a known divisor is zero. Before removing a list value, you can test membership. This style is often described as checking before acting.

Other failures are better handled at the operation itself. A file can disappear between a separate “does it exist?” check and the later open() call. Input conversion is often simplest to attempt directly and catch ValueError. Python code frequently follows an “attempt the operation and handle the expected exception” style when the operation itself is the most reliable source of truth.

Neither approach means “ignore errors.” The goal is to put the rule where it is clearest and most reliable. Predictable invalid input may deserve a check; a race-prone external operation may be safer to attempt inside try. The supporting lecture below compares these approaches in more detail.

Do not use exception handling to hide bugs

A handler should represent an intentional response to a failure you understand. Writing a broad except: around a large section of unrelated code can make debugging much harder because programming mistakes, typos, and unexpected states may be swallowed along with the one error you originally intended to catch.

Keep try blocks reasonably small and catch specific exception classes whenever practical. If an unexpected exception occurs, allowing its traceback to surface during development is often exactly what you want.

Common exception-handling mistakes

These failures were executed in Python. They demonstrate that a try statement only handles exceptions matched by its handlers, and that exception variables have intentionally limited lifetime.

Mistake 1 · No handler at all
number = int("abc")
Traceback (most recent call last):
  File "/tmp/ipykernel_313/1913867992.py", line 12, in run_err
    exec(compile(code, filename, "exec"), {})
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/awm_lesson15/mistake1_unhandled_valueerror.py", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'abc'

With no matching handler, the ValueError propagates normally and produces a traceback.

Mistake 2 · Catching the wrong exception type
try:
    number = int("abc")
except ZeroDivisionError:
    print("Division failed.")
Traceback (most recent call last):
  File "/tmp/ipykernel_313/1913867992.py", line 12, in run_err
    exec(compile(code, filename, "exec"), {})
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/awm_lesson15/mistake2_wrong_handler.py", line 2, in <module>
ValueError: invalid literal for int() with base 10: 'abc'

The handler exists, but it only matches ZeroDivisionError. The actual failure is ValueError, so it continues upward unhandled.

Mistake 3 · The reverse mismatch
try:
    result = 10 / 0
except ValueError:
    print("Bad value.")
Traceback (most recent call last):
  File "/tmp/ipykernel_313/1913867992.py", line 12, in run_err
    exec(compile(code, filename, "exec"), {})
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/awm_lesson15/mistake3_wrong_handler_zero.py", line 2, in <module>
ZeroDivisionError: division by zero

Again, the exception type matters. ValueError is not a catch-all category for “something went wrong.” Division by zero raises ZeroDivisionError.

Mistake 4 · Using the exception variable after its handler
try:
    number = int("abc")
except ValueError as error:
    print("Handled:", error)
print(error)
Traceback (most recent call last):
  File "/tmp/ipykernel_313/1913867992.py", line 12, in run_err
    exec(compile(code, filename, "exec"), {})
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/awm_lesson15/mistake4_exception_name_after_block.py", line 5, in <module>
NameError: name 'error' is not defined. Did you mean: 'OSError'?

Python clears the exception target after the handler finishes. Use information from the exception inside the handler, or explicitly copy the information you need to another variable.

Supporting classroom explanations

This page is original A Wandering Mind instructional material. The two CPSC 1301K lectures below reinforce Python’s exception-handling syntax and the design choice between checking a condition first and handling an exception after an attempted operation.

Exception Handling — by Hyrum Carroll · 11:39. Watch on YouTube ↗ · CPSC 1301K playlist ↗

What it adds: a classroom walkthrough of using exception handlers to respond to runtime failures without treating every expected problem as a fatal crash.

Error Checking Versus Exception Handling — by Hyrum Carroll · 10:16. Watch on YouTube ↗ · CPSC 1301K playlist ↗

What it adds: a direct comparison of checking for invalid conditions in advance versus attempting an operation and handling the exception it raises.

Try it yourself

For each exercise, identify the operation that might fail, the specific exception type you expect, and which statements should run only after success.

Exercise 1 · Convert safely

Set value = "42". Convert it with int() inside try. Print Invalid number. for ValueError; otherwise print the converted value plus one.

Show one answer
value = "42"
try:
    number = int(value)
except ValueError:
    print("Invalid number.")
else:
    print(number + 1)
43

Exercise 2 · Skip invalid values in a loop

Loop through ["8", "x", "2"]. Convert each value to an integer. Print twice the number when conversion succeeds and Skipped: x when it fails.

Show one answer
values = ["8", "x", "2"]
for value in values:
    try:
        number = int(value)
    except ValueError:
        print(f"Skipped: {value}")
    else:
        print(number * 2)
16
Skipped: x
4

Exercise 3 · Always print a completion message

Attempt 12 / 0. Handle ZeroDivisionError with Cannot divide by zero. and use finally to print Done..

Show one answer
try:
    result = 12 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")
finally:
    print("Done.")
Cannot divide by zero.
Done.

Module 4 complete: what comes next

You now have the core structure for handling predictable runtime failures: protected work in try, specific recovery paths in except, success-only work in else, and cleanup in finally. You also know that catching exceptions should make failures clearer and more deliberate—not hide bugs you do not understand.

The final module introduces another major built-in collection. Continue to Dictionary Keys, Values, and Methods to learn how Python maps unique keys to associated values instead of locating every item by numeric position.