Catching an error, and deciding whether you should
The shape
try:
value = int(entry)
except ValueError:
print(f"not a number: {entry!r}")
value = 0try holds the code that might fail. except names the failure you expected and says what to do instead. If nothing fails, the except block never runs.
There are two more clauses, and both are underused:
try:
f = open(path)
except FileNotFoundError:
print("missing")
else:
data = f.read() # runs only if no exception was raised
finally:
print("done") # runs either way, alwayselse holds the code that should run only on success. Keeping it out of the try means an unrelated failure inside data = f.read() is not accidentally caught by your except FileNotFoundError. finally runs whatever happens, including when the function returns or a different exception propagates, which makes it the place for cleanup.
Catch the specific exception
try:
result = risky()
except Exception:
passThis is the single most damaging pattern in Python. It hides typos, hides logic errors, hides the failure you did not think of, and leaves the program running on values that are quietly wrong. Every one of those bugs then surfaces somewhere else, with no traceback pointing at the real cause.
A bare except: is worse still, because it also catches KeyboardInterrupt and SystemExit — so Ctrl-C stops working and your program cannot be killed politely.
The rule: catch the exceptions you have a plan for. If you cannot say what you will do differently, do not catch it. An uncaught exception stops the program at the point of the mistake and prints exactly where — which is the most useful thing that can happen.
When you genuinely must catch broadly — a worker loop that must survive one bad row — log it and keep the detail:
for row in rows:
try:
process(row)
except Exception:
logger.exception("row failed: %s", row["id"])
failures += 1logger.exception records the full traceback. The loop continues, and you can still find out what happened.
Keep the try block small
try:
config = json.loads(text)
total = config["a"] + config["b"]
send(total)
except json.JSONDecodeError:
...Only the first line can raise JSONDecodeError, but a reader has to check three lines to know that. Worse, as the block grows someone adds a fourth line that raises the same error for a different reason, and the handler silently covers it. Put the risky call alone in the try and everything else in else or after it.
Ask forgiveness, not permission
Two ways to handle a key that might be missing:
if "name" in row: # look before you leap
name = row["name"]
else:
name = "unknown"try: # ask forgiveness
name = row["name"]
except KeyError:
name = "unknown"Python leans towards the second, for a concrete reason: the first checks and then acts, and between those two moments the world can change — a file that existed can be deleted, a key another thread was removing can disappear. For a dictionary in one thread it rarely matters, and row.get("name", "unknown") is better than both. For files, it matters a great deal, and try: open(...) except FileNotFoundError: is genuinely more correct than if path.exists().
Keeping the cause
When you catch one error and raise another, say what caused it:
try:
data = json.loads(text)
except json.JSONDecodeError as err:
raise ValueError(f"config file {path} is not valid JSON") from errfrom err chains them, so the traceback shows both: your readable message and the original parse failure with its position. Without from, Python still prints "During handling of the above exception, another exception occurred", which is noisier and reads like a bug in your handler.
To re-raise the same exception after doing something with it, use a bare raise:
except TimeoutError:
metrics.count("timeout")
raise # preserves the original traceback exactlyraise err also works but restarts the traceback from this line, losing where it actually happened.
The finally trap
def get():
try:
return 1
finally:
return 2 # returns 2, and swallows any exceptionA return in finally overrides everything, including an exception that was on its way up. Never return from finally. Use it for cleanup only — and for file and network cleanup, a with statement does the job better, which is a later lesson.
The one thing to keep
Catch only the exceptions you have a plan for, keep the `try` block down to the line that can actually raise, and chain with `from err` so the original cause survives in the traceback.
Before you move on
A nightly import wraps its whole 200-line body in `try: ... except Exception: logger.error("import failed")`. It reports failures but nobody can ever work out which record caused one. What is the core problem?
Pick the one you would defend. Nobody sees your answer.