This solved function has the exact structure you need. Read what it catches and, more importantly, what it does not.
def safe_len(value, default):
try:
return len(value)
except TypeError:
return default
# safe_len('hello', 0) -> 5
# safe_len(42, 0) -> 0 len() of a number raises TypeError
- Only the risky line goes inside try
The try block holds one expression. The smaller that block is, the harder it is for it to accidentally hide a failure you did not mean to handle — which is the entire risk of exception handling.
- The exception is named
except TypeError says what the author expected to go wrong. A reader learns something from it, and anything unexpected still travels up and gets seen instead of vanishing.
- return in both branches
No result variable, no falling through to a final return. Each path answers the question and leaves, so there is no state to trace.
Two different things can go wrong here, and they raise two different exceptions. Work out what int('abc') raises, then what int(None) raises. They are not the same.
int() raises ValueError when the text is not a number, and TypeError when the argument is not something it can read at all. One except clause can name both: except (ValueError, TypeError).
try: return int(text). except those two exception types: return default. That is the whole function — no if, no isdigit, no validation of your own.
def safe_int(text, default): try: return int(____) except (____, ____): return ____
def safe_int(text, default):
try:
return int(text)
except (ValueError, TypeError):
return default
`except:` catches everything, including KeyboardInterrupt when a user presses Ctrl-C, SystemExit when the program is shutting down, and the NameError from a typo three lines below. Bugs stop being visible and start being silent wrong answers. Naming the exceptions costs nothing and is the difference between handling a case and hiding a class of failure.
Both errors mean the same thing to this caller — 'not a number' — and the response is identical, so one clause says that clearly. Split them only when the handling genuinely differs.
Attempts built on isdigit() or a regex have to re-derive rules Python already knows: leading minus signs, surrounding whitespace, underscores in numeric literals. Asking forgiveness rather than permission is not just idiomatic here, it is more correct.
Do not reach for this when the caller needs to know the value was bad. A silent default is how corrupt data spreads quietly through a system — a missing price becomes 0 and the order ships free. Return None so the caller must decide, or let the exception travel, whenever 'unknown' and 'zero' are not the same thing.
- Add an optional base so safe_int('ff', 0, base=16) returns 255. Decide where the parameter goes and whether default should keep its position.
- Write safe_float alongside it, then notice how much is duplicated — factor out a single safe_call(fn, value, default) and judge whether the abstraction pays.
- Make a version that returns a (value, ok) pair instead of a bare default, so callers can tell a real 0 from a fallback 0.