Error boundaries & logging habits
Use logging instead of print and design where errors get caught.
Logging and where to handle errors
Why this matters
print is fine for a script you run once and watch. It falls apart in anything that runs unattended: a batch job, a web handler, a scheduled ETL. You cannot filter print by severity, cannot silence it in production without deleting lines, and cannot tell an error apart from a debug trace in a log file. logging fixes all three. The other half of robustness is deciding where a failure is handled. Get that wrong and one malformed row aborts a job that should have processed the other 99,999.
The logging model
A logger is a named channel. You grab one per module with logging.getLogger(__name__) and emit at a level: debug, info, warning, error, critical. Where those messages go (console, file, or both) and how verbose they are is configured once, at program start, not at each call site:
| Level | Use it for | Visible by default? | Who reads it |
|---|---|---|---|
| debug | values while tracing a problem | no | you, while debugging |
| info | normal milestones: job started, 500 rows written | no | you, reading yesterday's run |
| warning | something odd but survivable: a retry, a fallback | yes | whoever is on call |
| error | this operation failed | yes | whoever is on call |
| critical | the process cannot continue | yes | whoever gets paged |
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("processing %d records", len(records))
logger.warning("skipping bad record: %r", raw)
Note the %d and %r with trailing args instead of an f-string. logging interpolates the message only if the record is actually emitted (more on that below).
Error boundaries: raise low, catch high
Do not wrap every line in try/except. Decide the boundary that can actually recover. The common shape: a low-level helper raises on bad input, and the loop that owns the batch catches and skips, so one bad record does not sink the rest.
def safe_total(raws):
total = 0
for raw in raws:
try:
total += int(raw) # raises ValueError on "x"
except ValueError:
continue # skip; real code would logger.warning(...)
return total
print(safe_total(["1", "x", "3"])) # 4
int("x") raises ValueError, the loop swallows just that one, and 1 + 3 gives 4.
Pitfalls
- Catching too broadly.
except Exception:hides a typo'd name (NameError) alongside the errors you meant to skip; a bareexcept:is worse, also catchingKeyboardInterruptso you cannot even Ctrl-C out. Catch the specific type you expect (ValueError) and real bugs still surface. intis pickier than you think.int("3.5")raisesValueError(it is not an integer literal), sosafe_total(["3.5"])returns0, not3. Andint(None)raisesTypeError, whichexcept ValueErrorwill not catch at all.- Silent logs. A fresh logger's effective level defaults to
WARNING, sologger.info(...)prints nothing until you callbasicConfig(level=logging.INFO). "My logs vanished" is almost always this.
Interview nuance: prefer logger.info("n=%d", n) over logger.info(f"n={n}"). logging checks isEnabledFor(level) first and only formats the message if the record will actually be emitted, so the %-style call skips string building when that level is off. The f-string builds the string eagerly on every call, including calls that log nothing. On a hot path with expensive %r values, that difference is measurable.
safe_total loops over raw values, does total += int(raw), and guards it with except ValueError: continue. Sort each incoming value by what the loop does with it.
def safe_total(raws):
total = 0
for raw in raws:
try:
total += int(raw)
except ValueError:
continue
return total
print(safe_total(["1", "x", "3"])) # 4Apply
Your turn
The task this lesson builds to.
Warm-up (one file): implement safe_total(raws). Total the strings in raws that parse as
integers, skipping any that don't.
safe_total(["1", "x", "3"]) is 4.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement safe_total(raws) in processing/totals.py: use the read-only to_amount helper
(which raises ValueError on bad input) to total the valid values, skipping the rest. Some tests
are hidden.
3 hints and 2 automated checks are waiting in the workspace.