Chapter 18 of 24 | Python

Errors, debugging, and testing

Errors can be syntax errors, exceptions, or logic errors. Read the traceback from the bottom upward to find the exception type and message, then locate the line in your own code where the failure occurred.

def average(values):
    if not values:
        raise ValueError("values must not be empty")
    return sum(values) / len(values)

print(average([2, 4, 6]))

Raise a specific exception when a function's input violates its contract. Catch an exception only when you can recover, add useful context, or report a clear error. Avoid bare except: blocks that hide programming errors and interrupts.

Testing behavior

import unittest

class AverageTests(unittest.TestCase):
    def test_average(self):
        self.assertEqual(average([2, 4, 6]), 4)

    def test_empty_input(self):
        with self.assertRaises(ValueError):
            average([])

if __name__ == "__main__":
    unittest.main()

Tests should cover normal inputs, boundaries, and expected failures. A unit test checks a small function or class; an integration test checks cooperating components. Tests should be repeatable and independent of network availability, wall-clock time, and the developer's personal files whenever practical.

Debugging method

  1. Reproduce the problem with the same input.
  2. Read the exception type, message, and traceback.
  3. Inspect the values at the boundary where behavior becomes incorrect.
  4. Make one focused change and rerun the reproduction.
  5. Add a regression test for the fixed behavior.

Use a debugger or carefully placed logging to inspect state. Logging is better than scattered prints in long-running applications, but should not leak secrets or overwhelm the user with irrelevant detail.

Assertions, properties, and fuzzing

An assert documents an internal assumption during development; Python can disable assertions with optimization options, so do not use them to validate user input or enforce security checks. Property-based testing checks general laws over generated values. Fuzzing explores unusual inputs and can reveal crashes or parsing bugs. These techniques complement examples but do not prove all behavior correct.

Advanced quality practices

Static type checking, linting, coverage, mutation testing, and profiling answer different questions. Coverage shows which code executed, not whether assertions were strong. Mutation testing checks whether tests notice deliberate changes. A mature test suite combines focused unit tests, integration checks, boundary cases, and a reproducible environment.

Beginner-to-master progression

Exercises

  1. Add tests for empty input and a one-item average.
  2. Introduce a bug, capture a failing test, and fix it without changing unrelated behavior.
  3. Write a property for a sorting function and test it over generated lists.
  4. Compare line coverage with mutation testing and explain what each result establishes.

Test doubles and dependency boundaries

Tests sometimes need to control a clock, network client, or file system. Inject a small dependency interface rather than patching unrelated global state everywhere. A fake implementation can make a test deterministic; a mock can assert interactions but can also over-couple a test to internal call structure. Prefer asserting externally meaningful behavior.

Patch objects where the code under test looks them up, not necessarily where they were originally defined. Always clean up patches through a context manager or test framework fixture so one test cannot contaminate another.

Property-based tests and shrinking

Property-based frameworks generate values and shrink failing examples to simpler counterexamples. A good property captures a domain rule, such as a parser/serializer round trip or sorted output being monotonic. Constrain generated data to the valid input domain when testing successful behavior, and separately test rejection of invalid input.

A property can be wrong or too weak. For example, checking that a sort returns a sorted list does not prove it preserved every input element. Add multiset preservation or compare against a trusted reference. Independent properties reduce the chance of duplicating the same bug in both implementation and test.

Fuzzing and failure minimization

Fuzz targets should be small, deterministic, and robust to arbitrary bytes. Assert properties such as “does not crash,” “never returns an invalid object,” or “valid input round-trips.” Use time and memory limits when parsing could be expensive. When a fuzz run finds a failure, keep the minimized input as a regression case and verify the failure disappears under the same instrumented build.

Test suite architecture

Fast unit tests give quick feedback; integration tests cover module boundaries; end-to-end tests validate user workflows. Separate deterministic logic from slow or flaky external systems. Quarantine flaky tests only as a temporary, visible measure—the long-term goal is to identify uncontrolled time, concurrency, network, or filesystem assumptions.

Mastery checkpoint

  1. Replace a global dependency with an injected interface and test it with a deterministic fake.
  2. Write a property strong enough to check both ordering and element preservation.
  3. Build a fuzz target for a parser with resource limits and a regression corpus.
  4. Classify tests by unit, integration, or end-to-end level and explain their expected speed and scope.
0% read