Unit 1 · Fundamentals

Lesson · Unit 1 · 8 min read

Errors and exceptions, why crashes are useful information.

When Python crashes, it doesn't fail silently — it tells you exactly where things went wrong. Once you know how to read a traceback and when to catch errors instead of preventing them, your debug loop gets a lot shorter.

Section · 01

Two kinds of errors

Python errors come in two flavors, and the difference matters:

# SYNTAX ERROR — your code isn't valid Python. The program won't even start.
if x > 5
    print("big")
#                  ^
# SyntaxError: expected ':'

# EXCEPTION — your code is valid but something goes wrong at runtime.
x = int("hello")
#               ^
# ValueError: invalid literal for int() with base 10: 'hello'

Syntax errors mean “Python can’t even read your code.” Fix them and the program will at least start. You cannot catch a syntax error with try/except because the file never runs in the first place.

Exceptions are runtime events. The program ran a while, then something went wrong — a file was missing, a number wasn’t actually a number, a key didn’t exist in a dict. These you can catch.

Section · 02

Reading a traceback

When an exception is raised and you don’t catch it, Python prints a traceback — a stack of where the error came from — and exits. Read it bottom-up:

Traceback (most recent call last):
  File "checkout.py", line 23, in <module>
    apply_discount(cart, "VIP")
  File "checkout.py", line 14, in apply_discount
    rate = DISCOUNTS[code]
KeyError: 'VIP'

The last line tells you what went wrong: KeyError: 'VIP'means “you asked for the key 'VIP' in a dictionary and it wasn’t there.” The lines above show how Python got there — the deepest call is at the bottom. Most beginners panic and scroll up; read from the bottom and you’ll save yourself ten minutes per error.

Section · 03

The five exceptions you'll see most

ValueError       — wrong type of value
    int("hello")             # 'hello' isn't an int-string

TypeError        — wrong type entirely
    "abc" + 5                # can't add a string and an int

KeyError         — missing dictionary key
    prices["missing-item"]

IndexError       — list index out of range
    items[99]                # only 5 items in the list

FileNotFoundError — open() can't find the file
    open("nope.txt")

The names are self-documenting. When you see one, the fix is almost always: validate the input, or use a safer accessor like dict.get(key, default).

Section · 04

Catching exceptions with try / except

When you can’t prevent an error — say, the user typed something weird, or a file might not exist — you try the risky code and except the specific failure:

while True:
    raw = input("Enter your age: ")
    try:
        age = int(raw)
        break                          # success — exit the loop
    except ValueError:
        print("That's not a number. Try again.")

You can name multiple exception types, and you can capture the exception object for logging:

try:
    data = open(filename).read()
    config = parse(data)
except FileNotFoundError:
    print(f"No config at {filename}, using defaults.")
    config = DEFAULT_CONFIG
except ValueError as err:
    print(f"Config is malformed: {err}")
    sys.exit(1)

else and finally

try:
    result = risky_call()
except SomeError:
    handle()
else:
    # runs only if the try block didn't raise
    save(result)
finally:
    # always runs, even if an exception escaped
    cleanup()

You’ll use finallyfor “always do this regardless” — close a file, release a lock, log a metric. Most of the time you don’t need else, but when you do, it’s clearer than putting code at the bottom of the try.

Section · 05

When to catch and when to let it crash

Two rules that’ll save you weeks of confusion:

1. Only catch what you know how to handle.

A bare except:that swallows every exception is how bugs become silent and undebuggable. Catch the specific class you’re actually prepared to deal with.

# WRONG — silently eats everything, including bugs in your own code
try:
    save_record(data)
except:
    pass

# RIGHT — catch only the failure you actually expect
try:
    save_record(data)
except IOError as err:
    logger.warning(f"save failed, will retry: {err}")
    queue_for_retry(data)

2. Let it crash early in development.

The wrong reflex is to wrap every line in try/except so your program “keeps running.” In development, a loud crash is a gift — it tells you exactly what to fix. Add exception handling at the boundaries of your program (where user input arrives, where external services are called), not throughout your business logic.

You’ll know you’ve hit the right level of exception handling when your code reads top-to-bottom for the happy path and recovers cleanly at clearly-marked seams. Until then, less is more.

Curriculum source

Lesson content is original to YorkSims. Topic structure aligns with Python for Everybody by Dr. Charles R. Severance (py4e.com), licensed under Creative Commons Attribution 3.0 Unported.