Things break. A file won't exist, a user will type letters where you expected a number, an API call will time out. The question isn't whether your program will hit an error eventually, it's what happens when it does.

The basic try/except

try:
    result = 10 / 0
except ZeroDivisionError:
    print("You can't divide by zero!")

Code that might fail goes inside try. If it raises an exception, Python jumps straight into the matching except block instead of crashing the whole program. Without this, that division would stop your entire script dead, printing a traceback and exiting.

Catching more than one kind of error

try:
    value = int(input("Enter a number: "))
    result = 10 / value
except ValueError:
    print("That wasn't a valid number.")
except ZeroDivisionError:
    print("You can't divide by zero!")

Stacking multiple except blocks lets you respond differently depending on exactly what went wrong, which is almost always better than lumping every possible failure into one generic message.

The as keyword, for when you need the details

try:
    open("missing.txt")
except FileNotFoundError as e:
    print(f"Could not open the file: {e}")

finally: the block that always runs

try:
    f = open("data.txt")
    # work with the file
finally:
    f.close()

Whatever happens inside try, whether it succeeds or blows up with an exception, the finally block always runs. It's the natural spot for cleanup work, like making sure a file gets closed. That said, for files specifically, the with block from the earlier files tutorial already handles this for you automatically, which is honestly the better habit to build.

Don't just write "except:" and move on

A bare except with no specific error type catches literally everything, including mistakes you probably didn't mean to hide, like a typo in a variable name. It's tempting when you just want the program to stop crashing, but it tends to bury real bugs rather than fix them. At the very least, catch Exception specifically rather than a totally bare except, and consider printing or logging what actually went wrong.

Raising your own errors

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount

Sometimes the most helpful thing your own code can do is fail loudly and clearly, right at the point something is obviously wrong, rather than quietly continuing with bad data and causing a much more confusing failure somewhere else later on.