try, except, else and finally
Four earlier lessons have deferred to this one. The guessing game crashes on
abc. The type conversion lesson admitted isdigit() was a half-measure. The
nesting lesson promised a third option for missing keys. Here it is.
The shape
try:
age = int(input("Your age: "))
print(f"Next year you will be {age + 1}")
except ValueError:
print("That was not a whole number.")
Python runs the try block. If nothing goes wrong, except is skipped
entirely. If a ValueError occurs anywhere in the block, execution jumps
immediately to except and the program carries on.
Your age: abc
That was not a whole number.
No traceback, no crash.
The moment an error occurs, the rest of the try block is abandoned. If
int() fails on line 2, line 3 never runs. That is the point — you do not
continue with a value you failed to produce.
Catch what you expect, not everything
The single most important rule here.
try:
value = int(user_input)
except:
print("Something went wrong")
That catches everything — including typos in your own code, KeyboardInterrupt
when the user presses Ctrl + C, and MemoryError. Your bug gets swallowed and
reported as "something went wrong", and you have made debugging harder rather
than the program safer.
except Exception: is only slightly better.
Name the error you are actually handling:
except ValueError:
Now a NameError from your own typo still crashes loudly, which is what you
want. You handle the failure you anticipated and nothing else.
Several kinds
try:
value = data["count"]
result = 100 / int(value)
except KeyError:
print("No count in the data.")
except ValueError:
print("Count was not a number.")
except ZeroDivisionError:
print("Count was zero.")
The first matching except runs; the others are skipped. Order matters only
when types are related, in which case the more specific must come first.
Group them when the response is the same:
except (ValueError, TypeError):
print("Could not use that value.")
Looking at the error
try:
value = int(user_input)
except ValueError as error:
print(f"Could not convert: {error}")
Could not convert: invalid literal for int() with base 10: 'abc'
as error gives you the exception object. Useful for logging — but be careful
showing raw messages to users. They are written for programmers and can leak
details about your system. Log the real message, show something human.
else and finally
try:
file = open("data.txt")
except FileNotFoundError:
print("No such file.")
else:
print("Opened successfully.")
contents = file.read()
finally:
print("Done.")
else runs only if no exception occurred. It is for the code that should
run on success but should not itself be protected by the try. Keeping the
try block as small as possible is good practice, and else is how you do it —
otherwise a ValueError from contents would be caught by an except meant for
the file opening.
finally always runs — success, failure, or even a return in the middle.
It is for cleanup: closing files, releasing connections.
In practice you will use with instead of finally for files, which is the
next module. finally still matters for anything with does not cover.
Fixing the earlier examples
The guessing game
Module 3 used isdigit() and admitted its limits — it rejects -5 as invalid
rather than out of range, and cannot handle 50.5 sensibly:
def ask_for_guess(remaining: int) -> int:
"""Ask for a guess, repeating until a valid one is given."""
while True:
raw = input(f"\nGuess ({remaining} left): ").strip()
try:
guess = int(raw)
except ValueError:
print("Please enter a whole number.")
continue
if not LOWEST <= guess <= HIGHEST:
print(f"Out of range. Pick between {LOWEST} and {HIGHEST}.")
continue
return guess
Now -5 converts successfully and is rejected by the range check with the
correct message, and 50.5 gets "please enter a whole number" rather than
silently failing an isdigit() test. The error handling matches the actual
problem.
Missing keys, several levels down
Module 4 offered chained .get() and an in check, and promised a third:
try:
staff = company["offices"]["mumbai"]["staff"]
except KeyError:
staff = 0
Compare with:
staff = company.get("offices", {}).get("mumbai", {}).get("staff", 0)
The try version reads as what you actually want: get this; if any part is
missing, use zero. It also gets no worse as the path deepens.
Ask forgiveness, not permission
Python leans towards trying and handling failure, rather than checking first. The two styles:
# check first
if "count" in data and str(data["count"]).isdigit():
value = int(data["count"])
else:
value = 0
# try it
try:
value = int(data["count"])
except (KeyError, ValueError):
value = 0
The second is shorter, and it has no gap between the check and the use. It also
handles cases the first forgot — None, a nested list, a float string.
This is idiomatic Python and worth adopting. The exception is when failure is expected rather than exceptional: exceptions are relatively slow, so inside a loop running a million times where half the keys are missing, a check is better. For normal code, try it and handle the failure.
Where not to use it
Do not use it to hide bugs.
try:
process_everything()
except Exception:
pass
That is a program that fails silently and lies about it. except: pass is
occasionally legitimate — deleting a file that may not exist — and should always
make you look twice.
Do not wrap enormous blocks. A try around forty lines with one except ValueError gives you no idea which line failed. Wrap the smallest thing that
can fail.
Do not use it for flow control where an if would do. try/except around
if x > 5 is nonsense; nothing there raises.
Practice
- Write the age example with
try/except ValueError. Test with25,abcand an empty input. - Write a bare
except:and prove it catches aNameErrorfrom your own typo. Then narrow it and watch your bug surface properly. - Write a calculator asking for two numbers and an operator, handling
ValueErrorandZeroDivisionErrorseparately with different messages. - Use
as errorto print the underlying message for aValueError. - Write a
try/except/else/finallythat opens a file, and run it once with the file present and once without. Note which parts run each time. - Rewrite the guessing game's input function using
try/exceptas above. Confirm-5now gives the range message rather than the number message. - Take the nested
company["offices"]["mumbai"]["staff"]lookup and write all three versions: chained.get(), anincheck, andtry/except. Decide which you would rather maintain. - Write
safe_divide(a, b)returningNoneon division by zero, and a caller that handlesNone. - Write
try: ... except Exception: passaround something broken. Observe that you get no information at all, and consider how you would debug it.
Next: raising your own exceptions, so your functions can refuse bad input instead of quietly doing the wrong thing.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship