RizTech Academy logo
RizTech Academy
Errors and DebuggingLesson 3 of 525 min

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

  1. Write the age example with try/except ValueError. Test with 25, abc and an empty input.
  2. Write a bare except: and prove it catches a NameError from your own typo. Then narrow it and watch your bug surface properly.
  3. Write a calculator asking for two numbers and an operator, handling ValueError and ZeroDivisionError separately with different messages.
  4. Use as error to print the underlying message for a ValueError.
  5. Write a try/except/else/finally that opens a file, and run it once with the file present and once without. Note which parts run each time.
  6. Rewrite the guessing game's input function using try/except as above. Confirm -5 now gives the range message rather than the number message.
  7. Take the nested company["offices"]["mumbai"]["staff"] lookup and write all three versions: chained .get(), an in check, and try/except. Decide which you would rather maintain.
  8. Write safe_divide(a, b) returning None on division by zero, and a caller that handles None.
  9. Write try: ... except Exception: pass around 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