How to Refactor Code with AI
Refactor means improve the shape of the code without changing what it does. With AI, you ask for clarity and tests — not a surprise rewrite of the whole app.
In short: lock behaviour with tests first, ask for a small named improvement, compare the diff, re-run tests. Ban “while you’re at it” feature changes.
Why refactor with AI
Messy helpers slow every future change. AI is fast at renaming, extracting functions and simplifying nested loops. It is also eager to change behaviour. Tests are the seatbelt.
What a safe refactor request includes
- Goal: “extract slugify”, “rename for clarity”, “remove duplication” — one goal.
- Freeze behaviour: “do not change outputs; keep public names unless I say so.”
- Scope: one file or one function first.
- Proof: existing tests, or ask the AI to add characterisation tests before editing.
How a safe refactor flows
Example: messy process() → slugify_many()
Refactor the function below. Goal: clearer name, typed Python 3.12, use regex for slugify, skip None/blank. Do NOT change the idea of the outputs for normal strings. Add a short docstring. Return code only.
def process(d):
o = []
for x in d:
if x is not None and str(x).strip() != "":
s = str(x).strip().lower().replace(" ", "-")
while "--" in s:
s = s.replace("--", "-")
o.append(s)
return oafter.py (tested here)import re
_SLUG = re.compile(r"[^a-z0-9]+")
def slugify_many(values: list[object]) -> list[str]:
"""Turn each non-empty value into a simple URL slug."""
out: list[str] = []
for raw in values:
if raw is None:
continue
text = str(raw).strip().lower()
if not text:
continue
slug = _SLUG.sub("-", text).strip("-")
if slug:
out.append(slug)
return outtest_after.pyfrom after import slugify_many
def test_basic():
assert slugify_many(["Pune City", " ", None, "Nashik--Road"]) == [
"pune-city",
"nashik-road",
]cd code/refactor
python3 -m pytest -q
# -> 1 passedHow to refactor step by step
- Get a green baseline (or write tests that pin today’s behaviour).
- Ask for one improvement: rename, extract, simplify — not all three plus features.
- Read every changed line. Reject new dependencies you did not request.
- Re-run tests. If behaviour shifted, either fix the prompt or discard the diff.
- Commit the refactor alone so reviews stay easy.
| Safe ask | Risky ask |
|---|---|
| “Extract a helper; keep outputs the same” | “Clean this up and also add caching and a CLI” |
| “Rename locals for clarity” | “Modernise the whole package” |
| “Remove duplication in these two functions” | “Rewrite in another language” |
Settings tip
- Project rules: “Refactors must not change public behaviour without a new test.”
- In Cursor / Copilot Agent: point at files with @ and say “only these files”.
- Prefer a strong model for big design refactors; use inline/fast for rename-only work.
Quick quiz
1. What must stay the same in a refactor?
Observable behaviour (outputs and public contracts), unless you explicitly change them.
2. What do you do before editing?
Have tests green, or add characterisation tests.
3. Why commit refactors alone?
So you can revert cleanly and reviewers see one intent.
4. What is a risky prompt?
Mixing cleanup with new features in one request.
Save tokens and money
Small scope = fewer tokens and safer diffs. Do not ask five models to rewrite the same module. One good pass + tests is enough.