Why this matters
Validation catches bad input, but things still go wrong. A file is missing. The disk is full. A value that passed the check at the door is still wrong for a calculation three functions later. Python reports these problems with exceptions.
Where you catch an exception decides what your program can do about it. Catch it too low, in a function that has no way to fix the problem, and the best you can do is hide it. Catch it too high, in main(), and you have lost the context that tells you what to do. Catch it nowhere, and the program ends with a traceback, which for a user is a crash and for an attacker is a free look at your code.
A hidden error is a security problem in its own right. If save_inventory() fails and nobody notices, the user walks away believing their data was saved. If remove_item() quietly ignores a bad name, the inventory slowly drifts away from reality. Part of writing a secure programs is to make sure it never fails silently.
The rule: Catch an exception in the function that can do something useful about it. If the function cannot fix the problem or give the user a sensible choice, let the exception propagate to a caller that can. Never catch an exception just to make it go away.
This principle is also design rule 4 in the Low-level Design lab, which has a class recording and more examples.
Benefits
- Every error is either handled or reported. Nothing disappears.
- Each function has a clear contract: “I handle these problems, and I raise these.”
- The code that talks to the user is the code that decides what the user sees.
- When something does fail, the traceback points at the real cause.
Red flags (violations)
except:orexcept Exception:followed bypass,return None, orreturn [].- A
tryblock that wraps twenty lines when only one of them can raise. print()inside a low-level function that should be returning or raising, because that is the only way the author could think to report a problem.- A function that catches an exception and immediately raises a new one with no added information.
main()wrapped in one gianttry/except Exceptionthat prints “Something went wrong.”
Example 1 - Swallowing an error
Problematic code
def save_inventory(items: dict, filename: str) -> None:
try:
with open(filename, "w") as f:
for name, quantity in items.items():
f.write(f"{name},{quantity}\n")
except:
pass # If it fails, oh well
If the folder does not exist, or the disk is full, or items contains something that is not a string, the function returns normally. The user sees “Saved!” and the data is gone.
Fixed by letting the error propagate
def save_inventory(items: dict, filename: str) -> None:
"""Write the inventory to filename. Raises OSError if the file cannot be written."""
with open(filename, "w") as f:
for name, quantity in items.items():
f.write(f"{name},{quantity}\n")
def save_and_quit(items: dict) -> None:
try:
save_inventory(items, "data/inventory.txt")
print("Saved.")
except OSError as e:
print("Could not save the inventory. Your changes are still in memory.")
print(f"Reason: {e.strerror}")
# Give the user a chance to fix the problem and try again
Why this is better: save_inventory() has no way to fix a disk problem, so it does not try. It lets OSError propagate. save_and_quit() is talking to the user, so it is the right place to catch the error, explain it, and offer a way forward. The try block is small and catches one specific exception.
Example 2 - Handling at the right level
Problematic code
def main():
items = {}
try:
while True:
choice = ask_menu_choice()
if choice == "1":
name = input("Item name: ")
quantity = int(input("Quantity: "))
items[name] = quantity
elif choice == "2":
name = input("Item name: ")
del items[name]
elif choice == "4":
save_inventory(items, "data/inventory.txt")
break
except Exception as e:
print(f"Error: {e}")
One try around the whole program. A typo in a quantity, a misspelled item name, and a full disk all land in the same except, and all of them end the program. The user loses everything they typed.
Fixed by handling each error where it can be fixed
def ask_quantity() -> int:
while True:
try:
return int(input("Quantity: "))
except ValueError:
print("Enter a whole number.")
def remove_item(items: dict) -> None:
name = input("Item name: ").strip()
try:
del items[name]
except KeyError:
print(f"There is no item named '{name}'.")
def main():
items = {}
while True:
choice = ask_menu_choice()
if choice == "1":
name = input("Item name: ").strip()
items[name] = ask_quantity()
elif choice == "2":
remove_item(items)
elif choice == "4":
try:
save_inventory(items, "data/inventory.txt")
break
except OSError as e:
print(f"Could not save: {e.strerror}. Fix the problem and choose 4 again.")
Why this is better: A bad quantity is fixed by asking again, inside ask_quantity(). A missing item name is reported, inside remove_item(), and the menu continues. A save failure is reported where the user can act on it, and the loop keeps running so nothing is lost. There is no except Exception anywhere, so a real bug still produces a traceback the developer can read.
How to apply this rule
- Ask what you can do about it. Before writing
except, decide what the function will do with the error. If the answer is “nothing,” do not catch it. - Catch specific exceptions.
except ValueError,except FileNotFoundError,except KeyError. Never a bareexcept:. - Keep
tryblocks small. Wrap only the line or two that can raise. Then theexceptcan be specific about what went wrong. - Let low-level functions raise. A function that reads or computes should raise on failure and say so in its docstring. The caller decides what to tell the user.
- Add context when you re-raise.
raise ValueError(f"Bad line {n} in {filename}") from ekeeps the original cause and adds what you know.
Knowledge Check
load_inventory()opens a file and getsFileNotFoundError. It has no way to create the file. What should it do?- Return an empty dictionary so the program can continue
- Print “File not found” and return
None - Let the exception propagate to the caller
- Catch it with
except:andpass
Answer
3 — the function cannot fix the problem, so it should not hide it. The caller, which may be able to offer a different file or start fresh, decides what to do.- Which of these is the strongest sign that errors are being swallowed?
- A docstring that says “Raises OSError”
except KeyError: print("No such item")inside a menu functionexcept Exception: return []- A
tryblock around a singleint()call
Answer
3 — catching every exception and returning a default hides the failure completely. The caller cannot tell an empty result from a broken one. - Why is a single
try/exceptaround the wholemain()loop a problem?- It is slow
- Every error, from a typo to a disk failure, gets the same treatment and ends the program
- Python does not allow
tryinsidewhile - It is not a problem; it is the recommended pattern
Answer
2 — the handler is too far from the error to do anything useful. Errors that could have been fixed by asking again instead terminate the program. - A function catches
ValueErrorand immediately runsraise ValueError("Error"). What is wrong?- Nothing
- The new exception throws away the original message and adds nothing
ValueErrorcannot be re-raised- The function should have used
except:instead
Answer
2 — re-raising with less information than you started with makes debugging harder. Either let the original propagate, or add context with `raise ... from e`.
Next up
Once an error reaches the code that talks to the user, the question becomes what to say. The next principle: avoid information leakage through error messages.