The try/except block is how you catch exceptions in Python.

The basic pattern

text: str = input("Enter a number: ")

try:
    number: int = int(text)
    print(f"Doubled: {number * 2}")
except ValueError:
    print("That wasn't a valid number.")

What this does:

  1. Python tries the code inside the try block.
  2. If everything works, the except block is skipped.
  3. If a ValueError is raised inside try, Python jumps to the matching except and runs it.
  4. After the except block, the program continues normally.

Run with a bad input:

Enter a number: hello
That wasn't a valid number.

Run with a good input:

Enter a number: 7
Doubled: 14

What if you don’t catch?

If an exception isn’t caught, it propagates up — passes through every function call on the way until something catches it or it reaches the top of the program (where it crashes).

def parse_age(text: str) -> int:
    return int(text)


def setup_user(text: str) -> dict[str, int]:
    age = parse_age(text)        # may raise ValueError
    return {"age": age}


try:
    user = setup_user("twenty")  # bubbles up from parse_age
except ValueError:
    print("Bad age")

int(text) raises the error inside parse_age. parse_age doesn’t catch it, so it propagates to setup_user, which also doesn’t catch — it bubbles up to the try at the top.

This is powerful: only the code that knows how to handle the error has to catch it.

Accessing the exception object

To inspect the error itself, capture it with as:

text: str = "hello"

try:
    number: int = int(text)
except ValueError as e:
    print(f"Couldn't parse: {e}")
Couldn't parse: invalid literal for int() with base 10: 'hello'

The e here is the exception instance, with all its attributes.

Catching the wrong thing

except only catches the exception types you list. Anything else still crashes the program:

try:
    nums: list[int] = [1, 2, 3]
    print(nums[10])
except ValueError:
    print("...")        # this won't run — we got IndexError, not ValueError

That’s a feature, not a bug. Catching the specific exception you expect prevents you from accidentally hiding bugs elsewhere.

Don’t catch bare except

You may see code like:

try:
    risky_thing()
except:
    pass

Avoid this. A bare except: catches everything, including:

  • KeyboardInterrupt (the user pressing Ctrl+C)
  • SystemExit (a clean program shutdown)
  • bugs in your code that should crash

Always catch a specific exception type:

try:
    risky_thing()
except ValueError:
    pass

# at worst, use Exception (which excludes KeyboardInterrupt and SystemExit)
try:
    risky_thing()
except Exception:
    pass

Ruff and other linters flag bare except: for this reason.

try/except in real code — the “Easier to Ask Forgiveness than Permission” pattern

There are two styles for handling possible failures:

  1. Check first — “look before you leap”
  2. Try and catch — “easier to ask forgiveness than permission” (EAFP)

Python culture leans heavily toward EAFP:

# "look before you leap"
if "name" in user:
    print(user["name"])

# EAFP
try:
    print(user["name"])
except KeyError:
    print("no name")

For dicts, the if "name" in user form is fine. But for files, network calls, and many other cases, EAFP is more reliable — the world can change between your check and your action (“the file existed when I asked, but was deleted before I opened it”).

A complete example

def parse_int(text: str) -> int | None:
    """Return the integer value of `text`, or None if it can't be parsed."""
    try:
        return int(text.strip())
    except ValueError:
        return None


print(parse_int("42"))      # 42
print(parse_int("hello"))   # None
print(parse_int(" 17 "))    # 17

A small helper that wraps the dangerous operation and gives back a safe value. This is a very common pattern.

What’s next

You can catch one error. Next, how to handle multiple different exceptions cleanly.

Toggle theme (T)