Refactoring

Reorganizing the codebase as it grows without breaking anything

Concepts

Signs that refactoring is needed

Separating concerns

Asking the LLM to refactor

Verifying nothing broke

Check for Understanding

What is the difference between a refactor and a feature addition, and why does the distinction matter?

A refactor changes structure without changing behavior; a feature addition changes behavior. The distinction matters because they carry different risks and require different reviews. A refactor should not change test results (all tests that passed before should still pass); a feature addition will add new tests. Mixing refactoring with new features in a single change makes both harder to review and debug.

If you move a function from one module to another, what else might break that a test suite would catch?

Any code that imports the function from the old location will fail with an ImportError. Tests that import and call the function directly will fail immediately. Tests that go through HTTP (using create_test_client) will fail if any handler imports the function from the old path. The test suite catches all of these, which is why running it after every structural change is non-negotiable during a refactor.

What makes a refactoring prompt to an LLM more likely to produce useful output?

A prompt that describes: (1) the current structure and what is wrong with it, (2) the desired structure and why it is better, and (3) any constraints (do not change public interfaces, do not add new dependencies). "Refactor this code" produces cosmetic changes. "Move all raw SQL strings into a queries.py module, one function per query, and update all imports" produces a targeted structural change that can be reviewed.

What is technical debt, and can you give an example of a shortcut from an earlier lesson that would create it?

Technical debt is work deferred today that must be done later—at higher cost, because the codebase has grown around the shortcut. Examples from this tutorial: hard-coded sample data left in place when the database was added; SQL query strings embedded in handler functions rather than a query layer; a data loader that is not idempotent, requiring a manual cleanup step before every run. Each is fine in its lesson but accumulates cost if left in production code.

Exercises

Split the routes module

Separate the table and search routes from the data-entry routes into two files (routes_display.py and routes_add.py, or names you choose). Update all imports. Run the full test suite and fix any failures before moving to the next step. Write a one-paragraph explanation of where the boundary between the two modules lies and why you drew it there.

Extract a query layer

Move all raw SQL strings from handler and route functions into a dedicated queries.py module, one function per query. Ask the LLM to help with the mechanical steps, but read the diff carefully: did it change any query logic while moving it? Run the test suite to verify behavior is unchanged.

Write a refactoring prompt

Choose one function in the current codebase that you think is too long or does too many things. Write a prompt that explains the problem and the desired outcome without writing the replacement code. Run the prompt and evaluate whether the LLM's output achieves the goal or merely rearranges the code cosmetically.

Measure before and after

Pick one module to refactor. Before starting, record its line count and the number of ruff violations it has. Perform the refactor and record the same metrics afterward. Write a brief description of what changed and whether the metrics improved. Discuss whether the metrics captured what actually improved.