Skip to content
Hello Python
8/8

Error Handling Basics

Topic 8 of 8, with 4 concept checks. try/except/finally, and raising your own exceptions

Separate expected failures from broken assumptions

Failure boundaries

Place exception handling around the operation that may legitimately fail, catch only the errors you can resolve, and preserve unexpected failures for diagnosis.

Core lesson 01

Prefer except Exception (or a specific type) over a bare except: — bare except also swallows KeyboardInterrupt and SystemExit.

Python's exception hierarchy has BaseException at the root, with Exception as one major branch and things like SystemExit and KeyboardInterrupt as siblings, not children, of Exception. A bare `except:` catches everything under BaseException, including those control-flow signals — which means Ctrl+C might get silently swallowed by an overly broad except block. `except Exception:` catches ordinary errors while letting those system-level signals propagate as intended.

Python example
try:
    result = 10 / 0
except Exception as e:
    print(f"Handled: {type(e).__name__}: {e}")

# avoid this pattern:
# try:
#     ...
# except:
#     pass   # hides real bugs AND Ctrl+C

What to remember

What's the difference between except Exception: and a bare except:?

Common footguns

  • Using bare `except: pass` to 'make errors go away' — this hides real bugs and makes debugging painful later.

Core lesson 02

finally always runs — success, exception, or even a return in try/except — making it the right place for guaranteed cleanup.

`finally` is Python's guarantee: whatever happens inside the `try`/`except` above it — normal completion, an exception, or even a `return`/`break` — the `finally` block still executes before control actually leaves. This makes it the correct place for cleanup that must happen no matter what, like releasing a lock or closing a file (though a `with` context manager is usually cleaner for that specific case).

Python example
def read_value(d, key):
    print("attempting lookup")
    try:
        return d[key]
    except KeyError:
        return None
    finally:
        print("cleanup always runs")

print(read_value({"a": 1}, "a"))
print(read_value({"a": 1}, "b"))

What to remember

What is the purpose of the finally block?

Common footguns

  • Putting a `return` inside `finally` — it silently overrides any return/exception from the try/except, which is almost always a bug.
try:                     Exception flow:
    risky()          ---> raised in risky()
except ValueError:
    handle()      <---- caught here, others propagate up
finally:
    cleanup()   <---- ALWAYS runs, success or failure

Core lesson 03

class MyError(Exception): pass, then raise MyError('message') — subclass Exception (or a more specific built-in) and raise it like any other.

Custom exceptions let calling code catch and handle *your* specific failure modes distinctly from generic errors. You define them by subclassing Exception (or a more specific existing exception if that's semantically closer), optionally adding extra attributes or overriding `__init__` to carry structured error data, not just a message string.

Python example
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        super().__init__(f"Cannot withdraw {amount}, balance is {balance}")
        self.balance = balance
        self.amount = amount

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount

try:
    withdraw(50, 100)
except InsufficientFundsError as e:
    print(e, "| shortfall:", e.amount - e.balance)

What to remember

How do you create and raise a custom exception?

Common footguns

  • Subclassing BaseException directly instead of Exception — this makes your exception harder to catch with the conventional `except Exception` and signals 'this is as severe as SystemExit', which is rarely intended.

Core lesson 04

It explicitly chains exceptions, showing both the new error and its original cause in the traceback — preserving debugging context instead of hiding it.

When you catch one exception and raise a different, more meaningful one in response, Python still remembers the original by default (as `__context__`) and prints 'During handling of the above exception, another exception occurred'. Using `raise NewError(...) from original` makes that relationship explicit and intentional, changing the message to 'The above exception was the direct cause of the following exception' — clearer for anyone reading the traceback later.

Python example
def parse_config(raw):
    try:
        return int(raw)
    except ValueError as e:
        raise RuntimeError(f"invalid config value: {raw!r}") from e

try:
    parse_config("not-a-number")
except RuntimeError as e:
    print(e)
    print("caused by:", e.__cause__)

What to remember

What does raise NewError(...) from original_error do?

Common footguns

  • Using `raise NewError(...)` with no `from` (or `from None`) when you actually want to suppress an unhelpful original traceback — `from None` explicitly says 'don't chain', which is different from forgetting to chain.

Python lab

Browser Python lab

Runtime · idle

Python loads on your first run. Your code stays in this browser.

Best practices

  • Catch specific exceptions, not bare except: — makes bugs visible instead of silently hidden.
  • Use finally (or a context manager) for guaranteed cleanup rather than hoping later code runs.
  • Don't use exceptions for routine control flow when a simple check would do.
  • Include a clear, actionable message whenever you raise an exception.

Apply the concept in Interview practice

ExceptionseasyHackerRank · O(1) per case

Wrap the division in try/except, catching ZeroDivisionError and ValueError separately and printing each exception's message.

Open problem
Incorrect RegexeasyHackerRank · O(1) per pattern

Try re.compile(pattern) inside try/except re.error, printing True on success and False on failure.

Open problem
String to Integer (atoi)mediumLeetCode #8 · O(n) time

Manually walk the string handling leading whitespace, an optional sign, then digits, clamping to the 32-bit signed range instead of raising — a good exercise in defensive parsing.

Open problem

Concept checks

Q01

What's the difference between except Exception: and a bare except:?

Q02

What is the purpose of the finally block?

Q03

How do you create and raise a custom exception?

Q04

What does raise NewError(...) from original_error do?