Exceptions A: Why Error Handling Is Needed, try and except

Lesson goals
  • Explain what an Exception is and what happens to the program when it is not handled
  • Write a try / except block that catches an error and handles it
  • Handle several kinds of Exceptions separately, and get the error’s details with as
  • Understand why an “empty” except that catches everything is a dangerous habit

What will we learn?

  • What an Exception is.
  • Why real software has to handle errors and not crash.
  • The try / except syntax.
  • Different handling for different kinds of errors.

Why do we need this?

Imagine a small calculator server: one client sends "hello" instead of a number, and the whole server crashes. All the clients who come after them get “Connection refused”.

In the real world:

  • A user types “abc” in an age field. The app should not close.
  • A file the program tries to read was deleted. It should report it, not crash.
  • The internet went down in the middle of a request. It is worth trying again, or showing a message.

Errors will happen: bad input, missing files, an unstable network. The only question is what the software does when they happen.

Explanation from the basics

What is an Exception?

An Exception is an object that Python “throws” (Raise) when a Runtime Error happens. All the errors we saw yesterday (ValueError, ZeroDivisionError, TypeError…) are Exceptions.

When an Exception is thrown:

  1. Python stops the current line immediately.
  2. It looks for someone who will “catch” (Catch) the error.
  3. If nobody catches it, the program crashes and a Traceback is printed.

try / except is the way to catch.

Analogy: a safety net in the circus

A tightrope walker (your code) may fall (an Exception). Without a net, the fall is the end of the show (a crash). try = “the show is above a net”. except = the net, which catches the fall, and then you decide what to do: try again, tell the audience, or move on to the next act.

How it works

  • try: — “try to execute this block”.
  • except SomeError: — “if a SomeError is thrown inside the try, jump here and execute this block instead of crashing”.
  • If no error was thrown, the except block is skipped.
  • After the try/except, the program continues as usual.

Syntax

try:
    # code that might fail
except ErrorType:
    # what to do if ErrorType happened

# several error types, each handled differently
try:
    ...
except ValueError:
    ...
except ZeroDivisionError:
    ...

# get the error object itself (its message)
except ValueError as error:
    print(error)

# same handling for several types
except (ValueError, TypeError):
    ...

First code example

week-2/safe_age.py:

text = input("Enter your age: ")

try:
    age = int(text)
    print("Next year you will be", age + 1)
except ValueError:
    print("That is not a whole number:", text)

print("Program continues normally.")

Run 1 (valid input):

Enter your age: 30
Next year you will be 31
Program continues normally.

Run 2 (invalid input):

Enter your age: abc
That is not a whole number: abc
Program continues normally.

Breaking down the code line by line

LineRun 1 (30)Run 2 (abc)
text = input(...)text = "30"text = "abc"
try:Enters the blockEnters the block
age = int(text)Succeeds: age = 30A ValueError is thrown, and Python jumps straight to except
print("Next year...")PrintedSkipped. We never reach it
except ValueError:Skipped (no error)Caught!
print("That is not...")Does not runPrinted
print("Program continues...")PrintedPrinted, and the program did not crash

The important point: the moment an error is thrown, the rest of the lines in the try do not run. That is why you put inside the try only what depends on the success.

Another example

A robust division calculator, with different handling for each kind of error and a loop that asks again. week-2/safe_divide.py:

def ask_number(prompt):
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, for example 12 or 3.5")

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Cannot divide by zero - returning 0")
        return 0

x = ask_number("First number: ")
y = ask_number("Second number: ")
print("Result:", divide(x, y))

Example run:

First number: ten
Please type a number, for example 12 or 3.5
First number: 10
Second number: 0
Cannot divide by zero - returning 0
Result: 0

Breakdown:

  • return float(text) inside a try — if the conversion succeeds, the function returns immediately and also leaves the loop. If it fails, except prints a message, and the loop asks again. That way you “force” the user into valid input.
  • Each function catches the error that is relevant to it: ask_number catches ValueError (input), divide catches ZeroDivisionError (calculation).
  • An open question: should divide return 0 on division by zero? Mathematically, that is a wrong answer! In many cases it is better not to catch it here, and let the caller decide. We will talk about it in the lesson on raise.

as error — getting the error’s details

try:
    number = int("12.5")
except ValueError as error:
    print("Conversion failed:", error)
Conversion failed: invalid literal for int() with base 10: '12.5'

error is the error object. Printing it shows the message that would have appeared on the last line of the Traceback.

Common mistake

Mistake 1: an empty except that catches everything.

try:
    age = int(input("Age: "))
    print("Double:", age * 2)
    reslt = age / 0
except:
    print("Something went wrong")

There are three different problems here (input, division by zero, and even a missing letter in a variable name), and all of them are hidden under “Something went wrong”. It is impossible to debug. And worst of all: an empty except: also catches KeyboardInterrupt, so even Ctrl + C will not stop a program stuck in a loop like this!

The rule: catch only specific errors that you know how to handle. All the others continue upwards and crash, and that is good, because that is how you discover bugs.

Mistake 2: a huge try.

try:
    # 40 lines of code...
except ValueError:
    print("Invalid value")

Which of the 40 lines failed? Unknown. Put in the try only the lines that may fail with the error you catch, as few as possible.

Mistake 3: “swallowing” an error silently.

try:
    price = float(text)
except ValueError:
    pass

pass means “do nothing”. The error disappears, and price may not exist at all, which will cause a confusing NameError a few lines later. Always do something: a message, an explained default value, or writing to a log.

Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

Exercise 1: Safe conversion to a number

Write a function to_number(text) that returns the text as a whole number, and if the text is not a number — returns None, without the program crashing.

print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))

Expected output:

42
None
7
Show solution
def to_number(text):
    try:
        return int(text)
    except ValueError:
        return None


print(to_number("42"))
print(to_number("hello"))
print(to_number("7"))
42
None
7

int("hello") throws a ValueError. Without try, the program would crash. With except ValueError we decide what happens instead.

Exercise 2: Different handling for each kind of error

Write a function safe_divide(a_text, b_text) that receives two texts, converts them to numbers and divides. It returns:

  • the result of the division, if everything is valid;
  • "numbers only" if one of the texts is not a number;
  • "cannot divide by zero" if the divisor is 0.
print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))

Expected output:

2.5
cannot divide by zero
numbers only
Show solution
def safe_divide(a_text, b_text):
    try:
        return int(a_text) / int(b_text)
    except ValueError:
        return "numbers only"
    except ZeroDivisionError:
        return "cannot divide by zero"


print(safe_divide("10", "4"))
print(safe_divide("10", "0"))
print(safe_divide("ten", "2"))
2.5
cannot divide by zero
numbers only

Each kind of error has its own except, with suitable handling. Python checks the except blocks in order and enters the first one that matches.

Exercise 3: Showing the error message

The following code fails with a ValueError. Wrap it in a try, catch the error with except ValueError as error, and print the text Could not convert: followed by the message Python saved in error:

number = int("12abc")

Expected output:

Could not convert: invalid literal for int() with base 10: '12abc'
Show solution
try:
    number = int("12abc")
except ValueError as error:
    print("Could not convert:", error)
Could not convert: invalid literal for int() with base 10: '12abc'

as error saves the error object in a variable. When you print it, you get the message Python created.

Exercise 4: Choosing from a list

You have a list of names. Write a function pick(names, position_text) that receives a position as text (starting from 1) and returns the name at that position. It returns "not a number" for input that is not a number, and "no such position" for a position that does not exist.

names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))

Expected output:

Avi
not a number
no such position
Show solution
def pick(names, position_text):
    try:
        position = int(position_text)
        return names[position - 1]
    except ValueError:
        return "not a number"
    except IndexError:
        return "no such position"


names = ["Dana", "Avi", "Noa"]
print(pick(names, "2"))
print(pick(names, "abc"))
print(pick(names, "9"))
Avi
not a number
no such position

Two different errors in two places: int(...) may throw a ValueError, and accessing the list may throw an IndexError.

Exercise 5: An except that catches everything

def read_age(text):
    try:
        return int(text)
    except:
        return 0
  1. Why is except: without an error type dangerous?
  2. Give an example of a real bug this code would hide.
  3. Fix the code.
Show solution
  1. An empty except: catches every error — including ones we did not think of — and hides them. The code continues as if everything is fine, with a wrong value.
  2. If someone passes None by mistake instead of text, a TypeError is thrown — a real bug in the code that called the function. Instead of seeing it, we silently get age 0. (Even Ctrl + C is caught by an empty except:.)
  3. Catch only the error we expect:
def read_age(text):
    try:
        return int(text)
    except ValueError:
        return 0


print(read_age("30"))
print(read_age("thirty"))
30
0

Summary

  • Exception = an error that Python throws at runtime. If nobody catches it, the program crashes.
  • try = “try”. except SomeError = “if this error happened, do this instead of crashing”.
  • The moment an error is thrown, the rest of the try is skipped.
  • except SomeError as error gives access to the error message.
  • Catch specific errors only, with a small try. No empty except: and no pass that swallows errors.

Official documentation