When something goes wrong
When Python cannot do what you asked, it raises an exception: float("abc") raises ValueError, {}["x"] raises KeyError, 1 / 0 raises ZeroDivisionError, opening a missing file raises FileNotFoundError. If nothing handles it, the program stops and prints a traceback, the chain of calls that led there, read from the bottom up. That is the right outcome for a real bug. For problems you can expect, such as a bad price typed by a user, you handle the exception with try and except.
# Handle an expected problem
def to_price(text):
try:
return float(text)
except ValueError:
return None
print(to_price("184.25")) # 184.25
print(to_price("abc")) # None
# An unhandled one stops the program with a traceback
# print(int("abc")) # ValueError: invalid literal for int()...Catch only what you expect
Name the exception type: except ValueError:. A bare except: or except Exception: also swallows bugs you never meant to hide, such as a TypeError caused by passing None, and the program carries on with wrong data. Several types can share a handler, except (ValueError, KeyError):, or have separate ones. Python tries the handlers in order and runs the first that matches. You can keep the error with except ValueError as error: and look at error.
# Specific handlers, tried in order
def read_quantity(text):
try:
return int(text)
except ValueError as error:
print("not a whole number:", error)
return 0
except TypeError:
print("expected text, got something else")
return 0
print(read_quantity("200")) # 200
print(read_quantity("2.5")) # prints the message, then 0
print(read_quantity(None)) # prints the message, then 0else and finally
Two optional parts complete the statement. else: runs only when the try block raised nothing, which keeps the risky line short and the follow-up code outside the protection. finally: runs always, whether there was an error or not, even after a return: use it for clean-up. Often a with block already does the clean-up for you, as with files.
# else runs on success, finally runs always
def describe(text):
try:
price = float(text)
except ValueError:
print("bad price")
else:
print("price is", price)
finally:
print("done with", repr(text))
describe("184.25")
# price is 184.25
# done with '184.25'
describe("abc")
# bad price
# done with 'abc'Raising your own errors
A function that is given a value it cannot accept should refuse loudly with raise, not quietly return something wrong: raise ValueError("price must be positive"). Choose the built-in type that fits (ValueError for a right type but a bad value, TypeError for a wrong type) and write a message that says what was wrong. The caller then decides whether to catch it. Inside an except block, a bare raise re-raises the error you just caught, handy when you only wanted to log it.
# Refuse bad input, let the caller decide
def parse_price(text):
price = float(text) # may raise ValueError itself
if not price > 0:
raise ValueError("price must be positive")
return price
for text in ["184.25", "-5", "abc"]:
try:
print(parse_price(text))
except ValueError as error:
print("rejected:", error)
# 184.25
# rejected: price must be positive
# rejected: could not convert string to float: 'abc'except that does nothing (except: pass) turns a crash you could have debugged into silently wrong numbers, which is far worse in trading code. Catch an error only where you know a sensible response: use a default, skip the item, retry, or report it and stop. Otherwise let it propagate to a place that does know.try/except SpecificError: handle the problems you expect, nothing more.elseruns when nothing was raised;finallyalways runs.raise ValueError("message")refuses bad input clearly.
Next up: modules and imports, how to reuse code from the standard library and from your own files.
Lab · what's graded
- Convert text without crashing with
safe_float25 pts - Refuse a bad price with
parse_price25 pts - Skip bad entries with
first_valid_price25 pts - Handle a missing or damaged file with
read_quantity25 pts