| layout | default |
|---|---|
| title | Errors, Logging, and Testing |
| parent | Lessons |
| nav_order | 9 |
| permalink | /lessons/errors-logging-testing/ |
| course_lesson | true |
| course_index | 09 |
| previous_page | /lessons/files-serialization/ |
| previous_title | Files and Serialization |
| next_page | /lessons/regular-expressions/ |
| next_title | Regular Expressions |
Reliable software makes failures visible and checks important behavior automatically.
An exception is an emergency signal moving up through function calls until suitable code handles it. A log is the application's diary. A test is a repeatable experiment that checks whether behavior still matches its promise.
Raise specific exceptions inside business logic and handle them at a boundary such as a CLI or API handler. Do not silently catch everything.
class InsufficientBalanceError(ValueError):
"""Raised when an account cannot complete a withdrawal."""
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > balance:
raise InsufficientBalanceError("Not enough balance")
return balance - amountThe domain function raises meaningful errors. A user-interface boundary translates them into messages:
try:
updated_balance = withdraw(500, 700)
except InsufficientBalanceError as error:
print(f"Withdrawal failed: {error}")Catch the narrowest useful exception. except Exception: is appropriate only at carefully designed boundaries where the error is logged and the program has a recovery policy.
try:
number = int(raw_value)
except ValueError as error:
raise ValueError("quantity must be an integer") from error
else:
print("Conversion succeeded")
finally:
print("Conversion attempt finished")elseruns only when thetryblock succeeds;finallyruns whether success or failure occurs;raise ... from errorkeeps the original cause visible.
Do not use exceptions for ordinary branching when a simple condition communicates the rule better.
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Application started")Logs should explain what happened without exposing passwords, tokens, or personal data.
| Level | Typical meaning |
|---|---|
DEBUG |
detailed diagnostic information |
INFO |
normal important event |
WARNING |
unexpected situation with continued operation |
ERROR |
operation failed |
CRITICAL |
application may be unable to continue |
Use placeholder arguments instead of building a string before the logger knows whether it needs the message:
logger.info("Loaded %s student records", record_count)Inside an exception handler, logger.exception(...) includes the traceback:
try:
process_file(path)
except OSError:
logger.exception("Could not process file %s", path)
raiseNever log passwords, tokens, payment details, or unnecessary personal data.
An assert states a programmer assumption. Python can remove assertions when run with optimization, so do not use them to validate user input, authorization, or other required runtime rules.
Tests may use assertions because a failed test should report a mismatch.
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5import unittest
class WithdrawTests(unittest.TestCase):
def test_withdraw_reduces_balance(self):
result = withdraw(500, 200)
self.assertEqual(result, 300)
def test_withdraw_rejects_large_amount(self):
with self.assertRaises(InsufficientBalanceError):
withdraw(500, 700)
if __name__ == "__main__":
unittest.main()Run it with python test_account.py. Third-party frameworks such as pytest offer concise syntax and fixtures, but understanding test structure matters more than the framework.
A readable test has three stages:
- Arrange the inputs and dependencies.
- Act by calling one behavior.
- Assert the observable result.
Test normal values, boundaries, invalid input, and important failure paths. Avoid testing private implementation details.
class FakeSender:
def __init__(self):
self.sent = []
def send(self, message):
self.sent.append(message)A small fake is often easier to understand than a complex mock. Inject it into the service and assert what the service asked it to do.
Bug 1: every error is hidden
try:
save_report()
except Exception:
passdef transfer(amount):
assert amount > 0def test_cart():
assert cart._items == ["pen"]Show Bug Hunter fixes
- Catch only expected errors, log useful safe context, and either recover or re-raise.
- Raise
ValueErroror a domain exception because runtime validation must always execute. - Check public behavior such as
cart.total_items()or iteration unless_itemsitself is the promised interface.
- Define a custom exception.
- Catch an exception at a CLI boundary.
- Add useful logging levels.
- Remove a secret from a log message.
- Write a test for a pure function.
- Test an invalid input path.
- Use a fixture or setup helper.
- Mock an external dependency.
- Explain assertion versus exception handling.
- Add tests to an earlier project.
Show hints
- Subclass
Exception. - Keep the service focused on raising or returning domain errors.
- Use
debug,info,warning, anderrorintentionally. - Log identifiers, not credentials.
- Choose one input and expected output.
- Test the error or recovery behavior, not only the happy path.
- Share setup without hiding what the test needs.
- Replace the dependency with a predictable fake.
- Assertions catch programmer assumptions; exceptions handle runtime situations.
- Test business rules before integration details.
Show solution ideas
class InvalidExpense(ValueError): pass.- Catch it where you can show a user-friendly message.
- Configure a named logger and appropriate level.
- Replace the secret with a safe identifier or omit it.
- Assert the public behavior of the function.
- Use
pytest.raises(...)orassertRaises. - Use a fixture for repeated test data.
- Inject a fake client that returns known data.
- Use assertions during development; handle expected runtime errors explicitly.
- Add tests for normal, boundary, invalid, and failure cases.
Add structured logging and a test suite to the expense service. Include at least one test for each important failure mode.
Explain where exceptions should be raised, where they should be handled, and what a useful log message contains.