In [74]:
! pip install typeguard rollbar returns tenacity > /dev/null 2>&1
In [47]:
import contextlib
import json
import logging
import pathlib
import os
from typing import Union
import requests
from typeguard import typechecked
In [48]:
# Naive code snippets
def get_relevant_restaurants(user):
base_url = "https://en.wikipedia.org/wiki"
return requests.get(f"{base_url}/{user}").content
def get_user(path):
with open(path, 'r') as json_file:
return json.load(json_file)["user"]
def pick_best_restaurants(restaurants):
pass
In [49]:
def get_restaurant_recommendation(path):
user = get_user(path)
candidates = get_relevant_restaurants(user)
return pick_best_restaurants(candidates)
get_restaurant_recommendation("MY_AMAZING_FILE.json")
In [175]:
def get_restaurant_recommendation(path):
try:
user = get_user(path)
candidates = get_relevant_restaurants(user)
pick_best_restaurants(candidates)
except BaseException:
logging.error("VERY UNINFORMATIVE INFORMATION")
raise BaseException("VERY UNINFORMATIVE INFORMATION")
generaly it’s better for a program to fail fast and crash than to silence the error and continue running the program.
The bugs that inevitably happen later on will be harder to debug since they are far removed from the original cause.
Just because programmers often ignore error messages doesn’t mean the program should stop emitting them.
In [176]:
def get_restaurant_recommendation(path):
try:
user = get_config(path)["user"]
except FileNotFoundException:
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
except JSONDecodeError:
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
candidates = get_relevant_restaurants(user)
pick_best_restaurants(candidates)
When a file does not exists or when the file is not a valid json we raise FileNotFoundException and JSONDecodeError and log it away
We will reraise the same exact exception that occured instead raising a generic exception and allow the invoker to handle them diffrently.
Altough this code is far from pretty is much safer, we added deafault patiserie and the invoker of this function can destinguise between the diffrent types of errors and handle them in a diffrent manner if needed.
In [172]:
def get_restaurant_recommendation(path):
try:
user = get_user(path)
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
else:
candidates = get_relevant_restaurants(user)
pick_best_restaurants(candidates)
In [171]:
def get_restaurant_recommendation(path):
try:
user = get_user(path)
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
else:
candidates = get_relevant_restaurants(user)
pick_best_restaurants(candidates)
Secondly we can use else clause which occur when the try block executed and did not raise an exception.
Thirdly, we use dictionary builtin function get which allow us to define default values.
In [55]:
def run_unstopable_animation():
pass
This
In [56]:
try:
os.remove('somefile.pyc')
except FileNotFoundError:
pass
In [57]:
try:
run_unstopable_animation()
except KeyboardInterrupt:
pass
Becomes
In [58]:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove('somefile.pyc')
In [59]:
from contextlib import suppress
with suppress(KeyboardInterrupt):
run_unstopable_animation()
In [169]:
def get_user(path):
with open(path, 'r') as json_file:
return json.load(json_file)\
.get("user", "default_user")
def get_restaurant_recommendation(path):
try:
user = get_user(path)
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
else:
candidates = get_relevant_restaurants(user)
pick_best_restaurants(candidates)
In [170]:
def get_user(path):
with open(path, 'r') as json_file:
try:
user = json.load(json_file)\
.get("user", "default_user")
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
else:
return user
def get_restaurant_recommendation(path):
user = get_user(path)
candidates = get_relevant_restaurants(user)
pick_best_restaurants(candidates)
In [144]:
def get_user(path: Union[str, pathlib.PurePath]) -> str:
with open(path, 'r') as json_file:
try:
data = json.load(json_file)
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY INFORMATIVE INFORMATION")
raise
else:
return data.get("user","default_user")
In [145]:
@typechecked
def get_user(path: Union[str, pathlib.PurePath]) -> str:
with open(path, 'r') as json_file:
try:
data = json.load(json_file)
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY INFORMATIVE INFORMATION")
raise
else:
return data.get("user","default_user")
In [126]:
def get_relevant_restaurants(user):
base_url = "cool_restaurants.com"
resp = requests.get(f"{base_url}/{user}")
resp.raise_for_status()
return resp.content
In [127]:
def get_relevant_restaurants(user):
base_url = "cool_restaurants.com"
allowed_retries = 5
for i in range(allowed_retries):
try:
resp = requests.get(f"{base_url}/{user}")
resp.raise_for_status()
except (requests.ConnectionError):
if i == allowed_retries:
raise
else:
return resp.content
In [128]:
from functools import wraps
def retry(exceptions, allowed_retries=5):
def callable(func):
@wraps(func)
def wrapped(*args, **kwargs):
for i in range(allowed_retries):
try:
res = func()
except exceptions:
continue
else:
return res
return wrapped
return callable
In [129]:
@retry(exceptions=requests.ConnectionError)
def get_relevant_restaurants(country):
base_url = "cool_restaurants.com"
resp = requests.get(f"{base_url}/{user}")
resp.raise_for_status()
return resp.content
In [147]:
import tenacity
@tenacity.retry(retry=tenacity.retry_if_exception_type(ConnectionError))
def get_relevant_restaurants(user):
base_url = "cool_restaurants.com"
resp = requests.get(f"{base_url}/{user}")
resp.raise_for_status()
return resp.content
In [89]:
import sys
import rollbar
rollbar.init("Super Secret Token")
def rollbar_except_hook(exc_type, exc_value, traceback):
rollbar.report_exc_info((exc_type, exc_value, traceback))
sys.__excepthook__(exc_type, exc_value, traceback)
sys.excepthook = rollbar_except_hook
Lets say we have ValueError and we want to recover in diffrent way between TooBig/TooSmall.
Python default behavior
In [159]:
try:
1/0
except ZeroDivisionError:
raise
Replace exception type with both traces
In [161]:
try:
1/0
except ZeroDivisionError as e:
raise Exception from e
Replace exception type with only one trace
In [162]:
try:
1/0
except ZeroDivisionError as e:
raise Exception from None
In [ ]:
def pick_best_restaurants(user: str, candidates: List[str]) -> List[str]:
validate_user(user)
best_candicates = heapq.nlargest(5, valid_candidates)
update_df(user, best_candicates)
send_email()
In [174]:
def get_user(path):
with open(path, 'r') as json_file:
try:
user = json.load(json_file)\
.get("user", "default_user")
except (FileNotFoundException, JSONDecodeError):
logging.error("VERY UNINFORMATIVE INFORMATION")
raise
else:
try:
send_email(user)
else :
return user
through logging, reporting, and monitoring software.
In a world where regulation around personal data is constantly getting stricter,
In [90]:
def login(user):
raise CommonPasswordException(f"password: {password} is too common")
That being said, errors, whether in code form or simple error response, are a bit like getting a shot — unpleasant, but incredibly useful. Error codes are probably the most useful diagnostic element in the API space, and this is surprising, given how little attention we often pay them.
In general, the goal with error responses is to create a source of information to not only inform the user of a problem, but of the solution to that problem as well. Simply stating a problem does nothing to fix it – and the same is true of API failures.