try and except

Catching the errors you expect, letting through the ones you do not, and why a bare except is worse than no except at all.

try_except.py

try_except.py Python 3
Output

                    

bare_except.py

bare_except.py Python 3
Output

                    

Worth knowing

Catch the specific exception you expect: except ValueError:, not except:.
A bare except also swallows your typos, turning a crash into wrong output.
else runs when nothing raised. finally runs either way.
as e gives you the exception object, which carries the detail worth logging.

try and except: A Practical Guide

A try block runs code that might fail; an except block says what to do when a specific failure happens. The difficulty is not the syntax — it is being disciplined about which failures you actually catch.

The shape

try:
    return int(text)
except ValueError:
    return None

If int() raises ValueError, control jumps to the handler. If it raises anything else, the handler is skipped and the error keeps travelling up. That selectivity is the point.

Catch what you expect, nothing more

except:    # catches everything

This is almost always a mistake. It catches the error you were thinking of, and also your typos, your NameErrors, your AttributeErrors from a refactor you half-finished. The program stops crashing and starts producing wrong answers quietly, which is strictly worse: a crash tells you where to look.

The second program on this page has a deliberate typo inside a bare except. It returns 0, cheerfully, and nothing anywhere says a name was misspelled.

Name the exception:

except ValueError:
except (KeyError, IndexError):
except Exception as e:  # broad, but at least not BaseException

as e, and why you want it

except ValueError as e:
    print("could not convert:", e)

The exception object carries the detail — which key was missing, which value would not parse. Discarding it and printing "something went wrong" throws away the only part that would have helped.

else and finally

  • else runs when the try block raised nothing. It keeps the risky line alone in the try, so the handler cannot accidentally catch an error from the follow-up code.
  • finally runs either way, raised or not. It is where cleanup goes.

For files and locks, prefer with, which does the same job with less ceremony.

Raising your own

Handling is half of it. When a caller hands you something impossible, say so:

if age < 0:
    raise ValueError(f"age cannot be negative, got {age}")

Include the offending value in the message. "Invalid input" costs the next person a debugging session; "got -1" ends it immediately.

Ask forgiveness, not permission

Python leans toward trying the operation and handling the failure, rather than checking first. Checking if key in d before d[key] does the lookup twice and still has a gap between the check and the use. Try it and catch KeyError; it is faster in the common case and correct in all of them.

Which exceptions to expect

The instruction to catch specific exceptions is easy to agree with and harder to follow, because it requires knowing what a piece of code can raise. Three ways to find out, in order of reliability.

Read the documentation: the standard library is explicit about what its functions raise, and int(), open() and dict lookups all document theirs. Run it and see: trigger the failure deliberately once, read the traceback, and catch what actually appeared. And reason about it: an operation that parses can raise ValueError, one that touches the filesystem can raise OSError, one that indexes can raise IndexError or KeyError.

Catching Exception is the honest middle ground when you genuinely cannot enumerate them - a plugin boundary, a top-level handler that must not crash the program - and it is different from a bare except, because it still lets KeyboardInterrupt and SystemExit through. Those two are how a user stops your program, and swallowing them is how a program becomes impossible to quit.

Keep the try block small

A try that wraps twenty lines catches errors from all twenty, including ones you never thought about. If the handler says "could not parse the date", but the block also contains a database call, then a database failure now reports itself as a date problem, and the person debugging it starts in the wrong place.

Wrap the line that can fail, and put the follow-up work in else. The block stays honest about what it is handling.

Failing loudly is usually right

There is a reflex, especially early on, to wrap anything that has ever raised in a try and carry on. It feels defensive. What it actually does is convert a loud, located failure into a quiet, wrong result that surfaces somewhere else with no traceback pointing home.

An exception is not automatically a problem to suppress. It is information, and often the most useful information the program will produce. Handle the ones you have a real answer for - a missing optional file, a user typing letters into a number field, a network call worth retrying - and let the rest travel. A crash in development is a bug report that writes itself.

Cleanup belongs to with

finally guarantees that cleanup runs, and for files, locks and connections there is a better tool: a context manager. with open(...) closes the file whether the block succeeded or raised, without the extra indentation and without the risk of forgetting. Reach for finally when there is no context manager for what you are doing, and for with when there is.

A retry loop, whole

The most common real use of try outside parsing, with the loop else doing the "we ran out of attempts" branch:

attempts = []

def flaky():
    attempts.append(1)
    if len(attempts) < 3:
        raise ConnectionError("timed out")
    return "ok"


for attempt in range(1, 5):
    try:
        print("result:", flaky())
        break
    except ConnectionError as e:
        print(f"attempt {attempt} failed: {e}")
else:
    print("gave up")
attempt 1 failed: timed out
attempt 2 failed: timed out
result: ok

Four details are worth copying. The try contains only the call that can fail, so a bug in the printing would not be caught as a connection problem. The except names one specific exception, so a TypeError from a refactor still crashes loudly. The as e keeps the message, which is the part that tells you *why* it failed. And the break on success means the else runs only when every attempt was used, which is exactly the "gave up" case.

In production this would sleep between attempts, with the delay growing each time, and would only retry errors that are actually transient — a timeout, yes; a PermissionError, never, because retrying it will fail identically four more times and delay the real error.

The hierarchy, and what catching one gets you

Exceptions form a tree, and catching a class catches every subclass beneath it. That is what makes except Exception catch almost everything, and it is also how you catch a useful family without listing its members.

At the top is BaseException. Directly beneath it sit SystemExit, KeyboardInterrupt and GeneratorExit — the three that are *not* errors, but control flow. Exception holds everything else, and is what you should catch when you must catch broadly, because it leaves those three alone. This is the concrete difference between except Exception: and a bare except:, and it is the reason the bare form makes a program impossible to interrupt with Ctrl-C.

Below Exception, the groupings are useful. OSError covers FileNotFoundError, PermissionError, IsADirectoryError, TimeoutError and the rest of the filesystem and network family, so except OSError is a reasonable way to say "anything the operating system refused". ArithmeticError covers ZeroDivisionError and OverflowError. LookupError covers KeyError and IndexError, which is occasionally exactly what you want when reaching into a structure that might be short or missing a field.

Knowing the tree turns "catch specific exceptions" from a rule into something actionable: catch the narrowest class that covers the failures you actually have an answer for.

Raising with the cause attached

When you catch an exception and raise a different one, the original is worth keeping:

try:
    try:
        int("abc")
    except ValueError as exc:
        raise RuntimeError("could not read config") from exc
except RuntimeError as e:
    print(type(e).__name__ + ":", e)
    print("caused by:", type(e.__cause__).__name__)
RuntimeError: could not read config
caused by: ValueError

from exc sets __cause__, and the printed traceback then shows both: the low-level failure, the line "The above exception was the direct cause of the following exception", and your higher-level message. You get the *what* and the *why* in one traceback.

Without from, Python still attaches the original as __context__ and prints "During handling of the above exception, another exception occurred" — which is nearly as useful and reads as accidental rather than deliberate. raise ... from None suppresses it entirely, which is occasionally right when the internal error is noise a caller cannot act on.

The reason this matters is that translating exceptions is good practice. A library that lets a raw KeyError from its internal dictionary escape has leaked its implementation; one that raises ConfigError("missing 'host'") from exc has given the caller something to catch and the maintainer something to debug.

Exceptions of your own

Defining one is a single line, and the payoff is that callers can catch exactly your failure:

class ConfigError(Exception):
    pass

Subclass Exception, not BaseException. Name it for the problem rather than the place, and end it with Error by convention.

The reason to bother is selectivity. A caller who wants to handle a configuration problem and let everything else through cannot do that if you raised ValueError, because ValueError is also what int() raises three frames down. A dedicated class makes the handler precise.

A small hierarchy pays off in a library: one base class such as ThingError(Exception), with specific subclasses beneath it. Callers who want everything catch the base; callers who care about one case catch the subclass; and adding a new failure mode later does not break either.

Attach the data rather than only formatting it into the message. An exception that stores self.path or self.field lets a handler make a decision, where a handler given only a string has to parse English to find out what went wrong.

The middle ground between crashing and swallowing

The advice to let exceptions travel and the need for a program that keeps running are both real, and the space between them is where logging lives.

A handler has three honest choices. Handle it — you know what the failure means and what to do instead. Translate it — catch it, raise something more meaningful with from, and let it continue upward. Or record it and re-raise, which is what logging.exception is for: it writes the message and the full traceback to wherever logs go, and a bare raise afterwards sends the exception on its way.

try:
    process(record)
except ValueError:
    logging.exception("skipping record %r", record.id)
    continue

That pattern — log with the traceback, then skip this item and carry on — is the correct shape for batch work, where one bad record should not end a run over ten thousand. The crucial part is logging.exception rather than logging.error, because the first includes the traceback and the second throws it away.

What is not an honest choice is except Exception: pass. It is the one form that destroys information without replacing it, and the resulting program does not fail — it produces output that is quietly incomplete, which is the hardest kind of wrong to notice.

Questions people ask

What is the difference between except: and except Exception:? The bare form also catches KeyboardInterrupt and SystemExit, so it stops Ctrl-C from working. Never use it.

Can I catch several exceptions in one handler? Yes, with a tuple: except (KeyError, IndexError) as e:.

Does finally run if the try returns? Yes. It runs on every exit path, including return and break.

What happens if finally itself raises? Its exception replaces whatever was in flight, which is why finally blocks should be simple.

How do I re-raise after logging? A bare raise inside the handler re-raises the current exception with its original traceback intact.

Should I use exceptions for control flow? Python does, more than most languages — StopIteration ends every for loop. Use them for the exceptional path, not as a substitute for an if.

Is try/except slow? Setting up a try is nearly free; raising and catching is not. That is why "ask forgiveness" wins when failures are rare and loses when they are the common case.

What is an exception group? From Python 3.11, ExceptionGroup carries several exceptions at once — raised by concurrent code where more than one task failed — and except* handles them selectively.

Recap in one screen

  • Catch the narrowest exception you have an answer for; a bare except: also swallows Ctrl-C.
  • Keep the try block down to the line that can fail, and put the follow-up in else.
  • as e keeps the detail, which is the part that makes the message useful.
  • raise NewError(...) from exc preserves the cause, so the traceback shows both the what and the why.
  • An exception you cannot handle is information, not a problem — letting it travel beats a quiet wrong answer.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Why is a bare `except:` discouraged?

  2. When does an `else` block on a try run?

  3. What does `as e` give you?

Cheat sheet

try and except

A try block runs code that might fail; an except block says what to do when a specific failure happens. The difficulty is not the syntax — it is being disciplined about which failures you actually catch.

PYTHON · vizlearn.in/python/try_and_except.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.