Logging and Error Design Checkpoint
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Fix the mistakes
The slips that cost marks in this module. Spot each one, then check the fix.
Logging and error design work together. Errors travel up through the layers, and one place records them. This checkpoint builds a small payment flow and then tests your choices.
Design the errors first
List what can go wrong and give each case a type. A payment can be declined, or the gateway can be down. Both are PaymentErrors, so a caller can catch the base, or a child when it needs a different reaction.
class PaymentError(Exception): pass
class CardDeclined(PaymentError): pass
class GatewayDown(PaymentError): passExample 1 · Payment flow with a boundary
import loggingimport sys
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(levelname)s:%(name)s:%(message)s")log = logging.getLogger("pay")
class PaymentError(Exception): pass
class CardDeclined(PaymentError): pass
class GatewayDown(PaymentError): pass
def charge(amount): if amount > 1000: raise CardDeclined(f"limit exceeded for {amount}") if amount == 0: raise GatewayDown("timeout") return "paid"
def checkout(amount): try: return charge(amount) except CardDeclined as err: log.warning("declined: %s", err) return "Please use another card" except PaymentError as err: log.error("payment failed: %s", err) return "Try again later"
for amount in [200, 5000, 0]: print(checkout(amount))paid WARNING:pay:declined: limit exceeded for 5000 Please use another card ERROR:pay:payment failed: timeout Try again later
What this code does
- The specific handler for
CardDeclinedcomes first, with a lower log level. - Any other
PaymentErroris logged as an error and gets a safe reply. Each failure writes exactly one log line.
Declined means the customer must act. Gateway down means we can retry. Different reactions are the reason for different types.
Set up logging once
The application configures handlers: a screen handler at WARNING and a handler that keeps everything. Modules only call getLogger(__name__) and log. They never add handlers or call basicConfig.
Example 2 · Screen and file handlers
import ioimport loggingimport sys
log = logging.getLogger("app")log.setLevel(logging.DEBUG)screen = logging.StreamHandler(sys.stdout)screen.setLevel(logging.WARNING)screen.setFormatter(logging.Formatter("%(levelname)s %(message)s"))store = io.StringIO()keep = logging.StreamHandler(store)keep.setFormatter(logging.Formatter("%(name)s:%(levelname)s:%(message)s"))log.addHandler(screen)log.addHandler(keep)
log.info("start")log.error("disk full")print("--- stored ---")print(store.getvalue().strip())ERROR disk full --- stored --- app:INFO:start app:ERROR:disk full
What this code does
- The screen shows only the error. The stored copy keeps both records with the logger name.
- A
StringIOplays the part of a file.
Catch, retry, translate, log
The lowest layer raises. A middle layer may retry or translate with raise ... from. The boundary catches what is left, logs one ERROR line and returns a safe reply. A bug in the middle should not be hidden by a broad except.
Example 3 · Retry, then translate
class GatewayDown(Exception): pass
class OrderFailed(Exception): pass
def send(attempt): raise GatewayDown(f"try {attempt}")
def place(order_id): for attempt in (1, 2): try: return send(attempt) except GatewayDown as err: last = err raise OrderFailed(f"order {order_id} failed") from last
try: place(9)except OrderFailed as err: print(err, "| cause:", err.__cause__)order 9 failed | cause: try 2
What this code does
- The function retries twice, which is a real way to handle the error.
- After the last try it raises a domain error and keeps the last real error as its cause.
Finish up
Mark this lesson complete
This is the last lesson in this section.