WasiLearn Code
Python · When things go wrong

Catching errors with try and except

A crash is Python telling you something it could not do. Sometimes that is a bug to fix. Sometimes it is a situation you knew was possible — a file that is not there, a number the user typed as words — and the program should handle it and carry on.

typed = "twelve"

try:
    age = int(typed)
    print(f"Next year you will be {age + 1}")
except ValueError:
    print("Please type your age using digits.")
output
Please type your age using digits.

The try block runs. If it raises the kind of error named in except, the except block runs instead of the program stopping.

The shape

prices = [250, 120]

try:
    print(prices[5])
except IndexError:
    print("There is no item at that position.")
output
There is no item at that position.

Two things worth noticing. The try block stops at the failing line — anything after it inside try is skipped. And execution continues normally after the whole try/except, which is the point.

try:
    print("first")
    print(1 / 0)
    print("never runs")
except ZeroDivisionError:
    print("caught it")

print("carrying on")
output
first
caught it
carrying on

Catch the specific error

The single most important habit here. except on its own catches everything, including the bugs you wanted to hear about.

prices = [250, 120]

try:
    total = sum(prices)
    print(f"Totl: {totl}")
except:
    print("Something went wrong with the prices.")
output
Something went wrong with the prices.

There is nothing wrong with the prices. There is a typo — totl — and a bare except swallowed the NameError and printed a confident, wrong explanation. Now you have a program that reports a data problem every time it runs, and the real cause is invisible.

Name the error you are prepared for:

prices = [250, 120]

try:
    print(prices[5])
except IndexError:
    print("No item at that position.")
output
No item at that position.

Anything else still crashes, with a traceback pointing at the real line. That is what you want — you are handling one anticipated situation, not muting the program.

The errors worth knowing by name:

Exception Raised when
ValueError Right type, wrong value — int("abc")
TypeError Wrong type — "a" + 1
KeyError Dictionary key not present
IndexError List position out of range
ZeroDivisionError Division by zero
FileNotFoundError The file is not there
AttributeError The object has no such method

Several except blocks

def divide(text_a, text_b):
    try:
        return int(text_a) / int(text_b)
    except ValueError:
        return "Both must be numbers."
    except ZeroDivisionError:
        return "Cannot divide by zero."

print(divide("10", "2"))
print(divide("ten", "2"))
print(divide("10", "0"))
output
5.0
Both must be numbers.
Cannot divide by zero.

Python tries each except in order and runs the first that matches, so put specific exceptions before general ones. Exception matches almost everything, so it must come last if you use it at all.

To handle several the same way, use a tuple:

try:
    value = int("abc")
except (ValueError, TypeError):
    print("Could not read that as a number.")
output
Could not read that as a number.

Looking at the exception

as gives you the exception object, which carries the message.

try:
    int("twelve")
except ValueError as error:
    print(f"Could not convert: {error}")
output
Could not convert: invalid literal for int() with base 10: 'twelve'

Printing the message is far better than printing your own guess at what happened, especially in a log somebody else will read.

else and finally

else runs when the try block raised nothing. finally runs either way.

try:
    value = int("42")
except ValueError:
    print("bad input")
else:
    print(f"parsed {value}")
finally:
    print("done")
output
parsed 42
done
try:
    value = int("abc")
except ValueError:
    print("bad input")
else:
    print(f"parsed {value}")
finally:
    print("done")
output
bad input
done

else is worth using because it keeps the try block down to the one line that can actually fail. The wider the try, the more likely it catches something you did not mean it to.

finally runs even if the function returns or the exception is re-raised, which makes it the place for cleanup — closing a file, releasing a lock.

Most of the time a with block is better still:

with open("orders.txt") as f:
    data = f.read()
# closed automatically, error or not

Do not swallow errors silently

try:
    value = int("abc")
except ValueError:
    pass

print("finished")
output
finished

That runs cleanly and tells nobody anything. value was never assigned, so the next line that uses it fails somewhere unrelated with a NameError, and the real cause is a pass you wrote three files away.

If you genuinely want to ignore something, ignore it visibly:

def to_int(text, default=0):
    """Return text as an int, or default if it is not a number."""
    try:
        return int(text)
    except ValueError:
        return default

print(to_int("42"))
print(to_int("abc"))
print(to_int("abc", default=-1))
output
42
0
-1

The failure now produces a defined value, the caller chooses what it is, and nothing is hidden.

Ask forgiveness, not permission

Python culture prefers trying the thing and handling the failure over checking first. The habit even has an acronym — EAFP, easier to ask forgiveness than permission.

stock = {"chai": 12}

# checking first
if "coffee" in stock:
    print(stock["coffee"])
else:
    print(0)

# trying it
try:
    print(stock["coffee"])
except KeyError:
    print(0)

# or just
print(stock.get("coffee", 0))
output
0
0
0

All three work. The third is best here, because dictionaries already have the tool. The general point stands though: a try around the real operation is often clearer than a check that duplicates the operation's own logic — and it has no gap between the check and the use.

When to catch at all

Catch an exception when you can do something useful about it: substitute a default, ask again, try another source, or report it in terms the user understands.

If all you would do is print "an error occurred" and stop, let it crash. The traceback contains the file, the line and the cause, and that is far more use to whoever has to fix it than a message you invented.

Next: raising and asserting, which is the other side — signalling a problem from inside your own code.

Prefer learning with a teacher?

WasiLearn Academy runs small-group classes covering this material, online and in Karachi.

See the classes