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:
- Python tries the code inside the
tryblock. - If everything works, the
exceptblock is skipped. - If a
ValueErroris raised insidetry, Python jumps to the matchingexceptand runs it. - After the
exceptblock, 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:
- Check first — “look before you leap”
- 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.