Menu

Python Exceptions: try, except, else, finally, and raise

How to handle errors in Python — try/except/finally, catching specific exceptions, raising your own, and when to let an error propagate.

Errors Are Just Values With a Bad Attitude

When something goes wrong in Python — dividing by zero, reading a missing file, parsing a bad number — the runtime creates an exception object and starts unwinding the call stack until something catches it. If nothing does, your program terminates and prints a traceback.

Exceptions aren't themselves bad. They're how Python signals "I can't continue with this operation; here's why." Your job is to decide, case by case, which ones you know how to recover from and which you should let escalate.

The Basic Shape

main.py
Output
Click Run to see the output here.
  • try: opens the block of risky code.
  • except ValueError: catches that specific exception if it's raised inside the try.
  • If no exception fires, the except is skipped entirely.

Run the snippet with 42 — works. Run it with hello — the handler runs.

Catching Specific Exceptions

Python has a hierarchy of exception types. A few you'll meet often:

  • ValueError — a value was wrong in some way (int("abc"), out-of-range arguments).
  • TypeError — the wrong type was used ("hi" + 3).
  • KeyError — a dict key wasn't found.
  • IndexError — a sequence index was out of range.
  • FileNotFoundError — a file doesn't exist.
  • ZeroDivisionError — tried to divide by zero.
  • AttributeError — an object doesn't have the requested attribute.

Catch the specific one you know how to handle:

main.py
Output
Click Run to see the output here.

You can catch multiple exceptions in one clause by passing a tuple:

main.py
Output
Click Run to see the output here.

Notice as e. That binds the exception object to e so you can inspect its message or attributes.

Avoid Catching Everything

A bare except: catches literally anything, including KeyboardInterrupt (your Ctrl-C) and system-level exits. Don't use it.

except Exception: is a little better, but still dangerous — it swallows bugs you didn't anticipate and hides the true source of problems:

# Don't do this without a really good reason.
try:
    do_something()
except Exception:
    pass

The right move is almost always to catch the specific exception you know how to recover from. If an exception you didn't expect reaches the top of your program, the traceback tells you exactly what went wrong — that's a feature, not a bug.

else and finally

The try statement has two more optional clauses:

  • else runs if the try block finished without raising.
  • finally runs no matter what — exception or not.
main.py
Output
Click Run to see the output here.

else is the cleanest place for "success only" code — you don't want a try block doing more than the part that can actually fail. finally is for cleanup that must run even if the try fails: closing a resource, releasing a lock, restoring state.

Raising Exceptions Yourself

Use raise to signal an error in your own code:

main.py
Output
Click Run to see the output here.

Pick an exception type that fits what went wrong. Reach for the built-ins first — ValueError, TypeError, FileNotFoundError — before defining your own class.

Defining Your Own Exception

When the built-ins don't capture the meaning, define a custom exception:

main.py
Output
Click Run to see the output here.

Inherit from Exception (or a more specific built-in) and give the class a docstring. That's usually all you need. Custom exceptions let callers catch just the error meaningful to their domain.

raise ... from ...: Chained Exceptions

When one exception triggers another, keep the chain:

main.py
Output
Click Run to see the output here.

The from e attaches the original error. When the traceback prints, Python shows both — the ConfigError that surfaced and the FileNotFoundError that caused it. That kind of trail is invaluable when debugging.

Context Managers: The Cleaner Way to Clean Up

finally is fine, but for resources like files, a context manager (what with uses) is almost always better:

# finally version
f = open("data.txt")
try:
    data = f.read()
finally:
    f.close()

# with version
with open("data.txt") as f:
    data = f.read()

Both are safe. The with form is shorter and applies automatically. Reach for finally only when you're doing something the standard library doesn't already wrap in a context manager.

When Not to Catch

Catching an exception is a decision — you're saying "I can handle this." If you can't, let the exception propagate. Code like this is almost always a mistake:

try:
    do_work()
except Exception:
    pass  # silently ignore everything

Silencing errors makes bugs invisible. It's better to crash loudly than to limp along in an inconsistent state.

Wrap Up

  • try/except lets you handle errors you can recover from.
  • Catch specific exceptions, not Exception blankly.
  • raise signals errors in your own code.
  • with blocks replace most finally cleanup.
  • When in doubt, let the exception propagate.

Next: a tour of the specific errors Python raises most often — KeyError, ValueError, ModuleNotFoundError, and a few more — plus the debugging habits that fix them quickly.

Frequently Asked Questions

How do I handle errors in Python?

Wrap risky code in a try block and catch the specific exception in an except block: try: risky() except ValueError: .... Optional else runs if no exception occurred; finally runs either way, for cleanup.

Should I catch Exception to be safe?

No. A bare except: or except Exception: hides bugs you didn't anticipate. Catch the specific exception you know how to recover from, and let everything else propagate so you see the real problem.

What's the difference between raise and raise from?

raise NewError(...) raises a new exception. raise NewError(...) from original keeps the original exception attached as the cause, which Python shows in the traceback. Use from when a lower-level error triggered a higher-level one you want to surface.

Learn to code with Coddy

GET STARTED