All 24 modules are open from the start — nothing here is
locked, and nothing costs anything. Sign in so your progress, titles and
credentials stay with you, on every device you use.
Free forever, with your Google account. No password, no payment.
Up to now an error has ended the program. That is the right default —
a program that stops is easier to fix than one that carries on with a wrong
number. This module is about the cases where you can do better than stopping, and
about the far more common mistake: catching an error, hiding it, and leaving
whoever comes next with a symptom and no cause.
Ready?
1
Two Kinds of Wrong
A SyntaxError means Python could not read the code.
Nothing ran, and no amount of handling will help — the file has to be
fixed.
An exception is different. The code was fine; something
that happened while it was running was not. A file that is missing, a
number that will not parse, a key that is not there. Those you can
sometimes do something about.
Python runs the try block. If nothing goes wrong the
except is skipped entirely. If a ValueError is
raised, the rest of the try is abandoned and the
except runs instead.
The important word is abandoned. Everything after the failing
line inside the try is skipped, so a try block
wrapped around fifty lines tells you almost nothing about where things
stopped. Keep the block small — ideally the one line that can fail.
An exception is not a bug
int("abc") raising ValueError is
int working correctly. It was asked to do something
impossible and said so, immediately and precisely. The bug would have
been quietly returning 0.
Quick check
A try block has five lines and the second raises. How many of the remaining three run?
2
Catch the One You Can Actually Handle
except: on its own catches everything — including the typo
in your own code, the interrupt from the person trying to stop the
program, and the MemoryError nobody can do anything about.
try:
total = int(row["quantity"]) * price
except: # never do this
total = 0
If row is spelled rows, that is a
NameError, and this code turns it into a total of zero. The
program keeps going, the report is wrong, and there is nothing in the log
at all.
Name the exception you know how to deal with. Anything else should be
allowed to reach someone who can fix it:
except ValueError: # one
except (ValueError, KeyError): # either
except ValueError as error: # and keep the object
Several except blocks can follow one try, and
the first matching one wins — so the specific ones go above the general
ones, exactly like an elif chain.
Hides your typos
except Exception:
Better than a bare except:, still far too wide for most code.
Says what you expected
except ValueError:
Handles the case you thought about; everything else still surfaces.
Quick check
Why is a bare except: worse than no error handling at all?
3
else, finally, and the Exception Object
A full try statement has four parts, and the last two are
under-used:
try:
value = int(answer)
except ValueError as error:
print("could not read it:", error)
else:
print("worked:", value) # only when nothing was raised
finally:
print("this always runs") # raised or not
as error gives you the exception object.
str(error) is the message and
type(error).__name__ is the class name — between them, a log
line that says what actually happened rather than "something went wrong".
else holds the code that should run only when the
try succeeded. Putting it inside the try would
work, and would also mean an exception raised by that code gets
caught by an except written for something else entirely.
finally runs whatever happens — exception or not, return or
not. It is for cleanup that must happen: closing a file, releasing a lock,
putting a connection back. It runs even when the exception is on its way
up to crash the program.
Quick check
A function returns from inside a try that has a finally. Does the finally run?
4
Raising Your Own
raise reports a problem your own code has found. The rule of
thumb is to do it at the boundary — where data arrives —
so that everything below can trust what it is holding:
def parse_quantity(text):
quantity = int(text) # raises ValueError on rubbish
if quantity < 0:
raise ValueError(f"quantity must not be negative: {quantity}")
return quantity
Pick a class that means something. ValueError for a value
that is the right type and wrong; TypeError for the wrong
type entirely; KeyError for a missing key. And put the
offending value in the message — "invalid quantity" sends
someone hunting, "quantity must not be negative: -3" does
not.
Sometimes you catch an exception only to add context and send it on. A
bare raise re-raises the one you are handling, with its
original traceback intact:
try:
value = parse_quantity(cell)
except ValueError as error:
print(f"row {n}: {error}")
raise
The question worth asking
Before writing an except, ask what you will do in it. If
the answer is "carry on with a default", be sure that default is
genuinely correct and not just convenient. If the answer is "log it and
continue", make sure someone reads the log. If there is no good answer,
the right handling is none: let it stop.
A crash costs one debugging session. A swallowed exception costs a
quarter of wrong numbers and nobody knowing why.
Quick check
Which message is worth writing?
0 of 9 completed
Real Python runs right here in your browser — nothing to install, nothing
sent to a server. The interpreter downloads once the first time you press
Run, then stays cached.
01
To do
int("abc") raises a ValueError. That is
int working correctly — it was asked for something
impossible and said so.
If the try block finishes, the except is skipped
entirely. If it raises, the rest of the block is abandoned and the
except runs instead.
Your task: convert each answer to a whole number, using
0 for the ones that will not parse. Print one line per
answer, then the total:
12 0 7 0 19
your_code.py
PythonCtrl↵ to run
Hint
Wrap only the int() call in the try. quantity = int(answer) inside, quantity = 0 in the except. Keep the print and the total outside the try — neither of them can fail.
Output
02
To do
This code has a spelling mistake in it — quantitiy — and it
prints a report full of zeros without a word of complaint. The bare
except: catches the NameError along with
everything else.
That is the case against catching broadly: without the handler this would
have stopped on line one and told you exactly what was wrong.
Your task: narrow the except to
ValueError, then fix the NameError it exposes.
The output should be:
36 0 21 57
Each line is the quantity times three, with 0 for anything unparseable.
your_code.py
PythonCtrl↵ to run
Hint
Change `except:` to `except ValueError:` and run it. The NameError that appears names the misspelled variable, and the fix is one character.
Output
03
To do
as error hands you the exception object.
str(error) is the message Python wrote, and
type(error).__name__ is the class name.
Between them you can log a line that names the problem, which is the
difference between a report somebody can act on and one that says
"something went wrong".
Your task: for each answer, print either the number or a
line naming the exception and its message:
12 ValueError: invalid literal for int() with base 10: 'twelve' 7
your_code.py
PythonCtrl↵ to run
Hint
except ValueError as error: then print(f"{type(error).__name__}: {error}"). The f-string calls str() on the exception for you.
Output
04
To do
Several except blocks can follow one try, and
the first matching one wins. That lets each kind of failure get the
response it deserves rather than one shared fallback.
Your task:rate(hits, attempts) should hand
back the percentage. A non-numeric input is a ValueError and
should give -1; zero attempts is a
ZeroDivisionError and should give 0.0, because
nobody attempting anything is a real answer rather than bad data.
75.0 0.0 -1
your_code.py
PythonCtrl↵ to run
Hint
Convert both with int() and return int(hits) / int(attempts) * 100 inside the try. Then two excepts: ValueError returning -1 and ZeroDivisionError returning 0.0.
Output
05
To do
else runs only when the try raised nothing.
finally runs either way — exception or not, return or not.
else matters because code that belongs after a successful
attempt should not sit inside the try, where an exception it
raises would be caught by a handler written for something else.
finally is for cleanup that must happen: closing a file,
releasing a lock. It runs even while an exception is on its way up to
crash the program.
Your task: for each answer, print the number on success
or the failure on error, then done either way:
ok 12 done failed twelve done
your_code.py
PythonCtrl↵ to run
Hint
Four blocks: try, except ValueError, else, finally. The success line is print("ok", value) in the else — not in the try, where a print failing would be caught by the wrong handler.
Output
06
To do
raise reports a problem your own code has found. Doing it
where data arrives means everything below can trust what it is holding.
if quantity < 0: raise ValueError(f"quantity must not be negative: {quantity}")
Put the offending value in the message.
"invalid quantity" sends someone hunting;
"quantity must not be negative: -3" does not.
Your task: write parse_quantity(text). It
returns a whole number, and raises ValueError with a message
containing the offending value when the number is negative. Rubbish text
already raises ValueError from int(), so that
case needs nothing.
12 quantity must not be negative: -3
your_code.py
PythonCtrl↵ to run
Hint
quantity = int(text), then `if quantity < 0: raise ValueError(f"quantity must not be negative: {quantity}")`, then return quantity.
Output
07
To do
This function catches every failure and returns 0. The
import runs, the report is produced, and three of the numbers in it are
wrong with nothing anywhere to say so.
Handling an error means doing something about it. Returning a plausible
value and saying nothing is not handling — it is hiding.
Your task: keep the program running, but make each
failure visible. Print a line naming the bad value, count them, and leave
that row out of the total rather than counting it as zero:
skipped: twelve skipped: Total: 19 Skipped: 2
The blank value prints as an empty string after the colon and a space.
your_code.py
PythonCtrl↵ to run
Hint
Have to_quantity return None when it cannot parse, and narrow the except to ValueError. Then in the loop: if the result is None, print the skipped line and add one to skipped; otherwise add it to the total.
Output
08
To do
Two ways to deal with something that might not be there:
# check first if "region" in config: region = config["region"] else: region = "eu"
# or try it try: region = config["region"] except KeyError: region = "eu"
Python leans towards the second — it reads as one attempt rather than two
lookups, and it cannot go stale between the check and the use. For a
dictionary specifically, .get() says the same thing shorter,
which is why it exists.
Your task: write setting(config, key, fallback)
using try/except KeyError, then print three
calls:
9000 eu 30
your_code.py
PythonCtrl↵ to run
Hint
return config[key] inside the try, return fallback in the except KeyError. Square brackets are what raise — .get() would never give you a KeyError to catch.
Output
09
To do
Sometimes you catch an exception only to say something the deeper code
could not know — which row, which file, which user — and then let it
continue on its way.
A bare raise inside an except re-raises the
exception being handled, with its original traceback intact. Writing
raise error would work and would lose part of that.
except ValueError as error: print(f"row {n}: {error}") raise
Your task:load(rows) should print which row
failed and then let the exception through. The caller catches it, so the
program finishes:
row 3 failed: invalid literal for int() with base 10: 'x' stopped after 2 good rows
your_code.py
PythonCtrl↵ to run
Hint
Wrap the int(row) in a try. In `except ValueError as error:` print f"row {number} failed: {error}", then a bare `raise` on its own line.
Output
The import validator
To do
A supplier's export has to be loaded, and some of it is wrong. The job is
not to make the bad rows disappear — it is to load every good one, and
report every bad one precisely enough that somebody can fix the file.
Each row should be name,quantity,price. A row can be wrong in
four ways, and each needs its own message.
Write parse_row(row): it returns
(name, quantity, price) with the numbers converted, and raises
ValueError with a useful message when the row is not usable.
Not exactly three fields — expected 3 fields, got 2
A quantity or price that will not parse — let int()'s own ValueError through unchanged
A negative quantity — quantity must not be negative: -4
A price of zero or less — price must be positive: 0
Then load the file. Keep going after a bad row. Collect
the good ones into loaded as tuples, count the bad ones in
rejected, and print one line per bad row saying which it was and
why.
Print exactly six lines:
row 3: expected 3 fields, got 2 row 4: invalid literal for int() with base 10: 'x' row 5: quantity must not be negative: -4 row 6: price must be positive: 0 Loaded: 2 Value: 15630
Value is quantity times price, summed over the loaded rows
only.
your_code.py
PythonCtrl↵ to run
Hint
In parse_row: split on the comma, and if the number of fields is not 3, raise ValueError(f"expected 3 fields, got {len(fields)}"). Then int() both numbers — leave that call unwrapped so its own ValueError travels. Then check the quantity and the price and raise your own for each. In the loop, use enumerate(..., start=1) for the row number, wrap the parse_row call in try/except ValueError as error, print f"row {number}: {error}" and count it; on success append the tuple. The value is a sum over the loaded tuples.