Handling Exceptions

When something goes wrong at runtime — a missing file, bad input, a network failure — Python raises an exception. If nothing handles it, the program stops with a traceback. try/except lets you catch exceptions and respond sensibly instead.

try and except

PY try_except.py
def parse_age(text):
    try:
        age = int(text)
    except ValueError:
        print(f"{text!r} is not a number")
        return None
    return age

print(parse_age("42"))
print(parse_age("forty-two"))
Output
42
'forty-two' is not a number
None

Code in the try block runs normally. If it raises a ValueError, Python jumps to the matching except block instead of crashing.

Catching specific exceptions

PY specific.py
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return float("inf")
    except TypeError as err:
        return f"bad input: {err}"

print(safe_divide(10, 4))
print(safe_divide(1, 0))
print(safe_divide("10", 2))
Output
2.5
inf
bad input: unsupported operand type(s) for /: 'str' and 'int'

as err gives you the exception object, whose message you can show or log. Several exceptions can share one handler: except (ValueError, TypeError):.

Don't catch everything. A bare except: or except Exception: also swallows bugs you didn't anticipate — typos, wrong variable names — and makes them very hard to find. Catch the specific exceptions you expect and can actually handle.

else and finally

PY else_finally.py
def read_number(text):
    try:
        value = float(text)
    except ValueError:
        print("  invalid")
    else:
        print(f"  parsed {value}")
        return value
    finally:
        print("  (cleanup always runs)")

print("first:")
read_number("3.5")
print("second:")
read_number("abc")
Output
first:
  parsed 3.5
  (cleanup always runs)
second:
  invalid
  (cleanup always runs)
  • else runs only if the try block raised nothing. Putting follow-up code here keeps the try block small, so you don't accidentally catch errors from it.
  • finally runs no matter what — success, exception, or even a return. Use it for cleanup like closing connections.

Common built-in exceptions

ExceptionTypical cause
ValueErrorRight type, wrong value: int("abc")
TypeErrorWrong type: "a" + 1, calling a non-function
KeyErrorMissing dictionary key
IndexErrorList index out of range
AttributeErrorObject has no such attribute or method
NameErrorUndefined variable
ZeroDivisionErrorDividing by zero
FileNotFoundError, PermissionErrorFile system problems (subclasses of OSError)
ImportError / ModuleNotFoundErrorMissing module or package

The exception hierarchy

Exceptions are classes arranged in a hierarchy, and an except clause catches the named class and all its subclasses:

PY hierarchy.py
print(issubclass(FileNotFoundError, OSError))
print(issubclass(KeyError, LookupError), issubclass(IndexError, LookupError))

data = {"a": [1, 2]}
for key, index in [("a", 5), ("b", 0)]:
    try:
        print(data[key][index])
    except LookupError as err:
        print(f"lookup failed: {type(err).__name__} {err}")
Output
True
True True
lookup failed: IndexError list index out of range
lookup failed: KeyError 'b'

Because the first matching clause wins, put more specific exceptions before more general ones.

EAFP vs LBYL

Two styles exist for handling things that might fail. "Look before you leap" (LBYL) checks first; "easier to ask forgiveness than permission" (EAFP) just tries and handles the exception. EAFP is common in Python:

PY styles.py
config = {"port": "8080"}

# LBYL
if "port" in config and config["port"].isdigit():
    port = int(config["port"])

# EAFP
try:
    port = int(config["port"])
except (KeyError, ValueError):
    port = 80

print(port)
Output
8080

EAFP avoids duplicating checks and handles race conditions — a file could be deleted between checking it exists and opening it.

Exception groups

When several independent operations fail together — for example, concurrent tasks — Python can raise an ExceptionGroup, handled with except*:

PY groups.py
def validate(form):
    errors = []
    if not form.get("name"):
        errors.append(ValueError("name is required"))
    if "@" not in form.get("email", ""):
        errors.append(ValueError("email is invalid"))
    if errors:
        raise ExceptionGroup("form invalid", errors)

try:
    validate({"name": "", "email": "nope"})
except* ValueError as group:
    for err in group.exceptions:
        print("-", err)
Output
- name is required
- email is invalid

Try it yourself

Try it yourself
Console output