Refactoring
Reorganizing the codebase as it grows without breaking anything
Concepts
- A refactor changes the internal structure of code
without changing its observable behavior
- the test suite is the evidence that behavior is preserved
- without tests, a "refactor" is an unverified rewrite
- Separation of concerns: each module should
have one clear responsibility
- routes handle HTTP, query functions handle SQL, renderers handle HTML
- changes to one layer should not require changes to others
- Technical debt is the extra work created by shortcuts
taken earlier
- like financial debt, it accrues interest: the longer it is left, the harder it becomes to fix
- A refactoring prompt should specify the desired structure and the reason for it,
not just ask the LLM to "clean this up"
- vague prompts produce cosmetic changes that do not improve the structure
- Reviewing a refactor diff is the same activity as reviewing new code
- check that logic is unchanged, no edge cases were silently dropped, and the new structure actually achieves the stated goal
Signs that refactoring is needed
- Functions longer than a screen, repeated code, handlers that know too much about the database
- Reading the code with fresh eyes
Separating concerns
- Routes in one module, database queries in another, HTML rendering in a third
- The rationale and the mechanical steps
Asking the LLM to refactor
- Prompts that produce useful refactors versus prompts that produce churn
- Reviewing a refactor diff the same way as new code
Verifying nothing broke
- Running all tests after each refactor step
- The value of a test suite as a safety net during reorganization
- What technical debt is, how it accumulates, and when to pay it down
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.