💻 Coding
Code Refactoring Prompt: Tests First, Small Safe Steps
Paste code, versions, and constraints for a ChatGPT refactoring plan: behavior list, characterization tests, named moves, small commits, and a first diff.
0Reviews
Prompt
Act as a senior software engineer who plans behavior-preserving refactors in small, reviewable commits, and who uses only functions, classes, and library calls that appear in the pasted code or in the standard library of the stated language version. Inputs: - Code to refactor, with file paths: [CodeUnderChange] - Language, runtime, framework, and library versions: [StackVersions] - What the refactor should achieve (readability, testability, splitting a module, removing duplication): [RefactorGoal] - Existing tests that cover this code, or none: [ExistingTests] - Constraints I cannot break (public signatures, performance budget, no new dependencies, deadline): [HardConstraints] - Code smells that bother me, in my words: [CodeSmells] Generate: 1. Behavior inventory: every behavior of CodeUnderChange, including edge cases, error paths, side effects, and return types, written as plain statements I can check. 2. Characterization tests: where ExistingTests are missing or thin, list test names and assertions that pin current behavior, including odd behavior, before anything changes. Write them in the test framework I named, or ask which one. 3. Smell-to-refactoring map: link each CodeSmell, and any you see, to a named refactoring (Extract Function, Replace Conditional with Polymorphism, Introduce Parameter Object, Replace Magic Number with Constant) and the lines it touches. 4. Commit sequence: ordered small steps toward RefactorGoal, each one compiling and passing all tests on its own, with a one-line commit message. 5. Diff for step 1 only, as a unified diff against the pasted code. 6. Risk notes: behavior that could change by accident (float rounding, ordering, mutation of inputs, exception types) and which test catches it. 7. API honesty check: list every function or method used in the plan and mark it present in pasted code, standard library, or UNVERIFIED for me to confirm. Rules: - Do not change behavior, even bugs, during the refactor. List suspected bugs separately for a later fix. - Never invent helper libraries, framework methods, or config files. - Respect HardConstraints; if RefactorGoal conflicts with them, say so and stop at the safe part.
Instructions
Paste real code and versions. Commit the characterization tests first and run them green, then apply one step at a time. Keep suspected bugs on a separate list. Run the full test suite before you start and after every step.
Generated Output
This image was generated using the prompt above.

Examples
Example Input
CodeUnderChange (shipping/rates.py):
def shipping_cost(order):
if order["country"] == "US":
if order["weight"] <= 1:
cost = 5.0
elif order["weight"] <= 5:
cost = 9.5
else:
cost = 9.5 + (order["weight"] - 5) * 1.2
else:
if order["weight"] <= 1:
cost = 14.0
else:
cost = 14.0 + (order["weight"] - 1) * 3.0
if order.get("express"):
cost = cost * 1.5
return round(cost, 2)
StackVersions: Python 3.11, pytest 8, no framework
RefactorGoal: make it easy to add Canada rates next month without another nested if
ExistingTests: none
HardConstraints: keep the signature shipping_cost(order) and the float return; no new dependencies
CodeSmells: nested ifs, magic numbers, US logic and international logic tangledExample Output
1. BEHAVIOR INVENTORY
- US, weight <= 1: 5.00
- US, 1 < weight <= 5: 9.50
- US, weight > 5: 9.50 + (weight - 5) * 1.2
- Any non-US country, weight <= 1: 14.00
- Non-US, weight > 1: 14.00 + (weight - 1) * 3.0
- express truthy: cost * 1.5, applied after the base rate
- Result rounded with round(cost, 2) (banker's rounding on exact halves)
- Missing "country" or "weight": raises KeyError. Missing "express": treated as False.
2. CHARACTERIZATION TESTS (pytest)
- test_us_light_is_5: weight 1 -> 5.0
- test_us_mid_is_9_5: weight 5 -> 9.5
- test_us_heavy_per_kg: weight 7 -> 11.9
- test_intl_light_is_14: country "DE", weight 0.5 -> 14.0
- test_intl_per_kg: country "DE", weight 3 -> 20.0
- test_express_multiplies_after_base: US, weight 7, express True -> 17.85
- test_missing_weight_raises_keyerror
- test_country_case_sensitive: country "us" is charged international (current behavior)
3. SMELL MAP
- Magic numbers -> Replace Magic Number with Constant (lines 4 to 13)
- Nested country/weight ifs -> Extract Function per zone, then a zone table (lines 2 to 13)
- Tangled US and international logic -> Separate into us_base() and intl_base()
4. COMMIT SEQUENCE
1. Add characterization tests (no code change).
2. Extract us_base(weight) and intl_base(weight); shipping_cost calls them.
3. Move numbers into named constants per zone.
4. Add a RATE_FUNCS dict mapping "US" to us_base with intl_base as the default.
5. Next month: add "CA": ca_base with its own tests.
5. DIFF FOR STEP 2
--- a/shipping/rates.py
+++ b/shipping/rates.py
@@
+def us_base(weight):
+ if weight <= 1:
+ return 5.0
+ if weight <= 5:
+ return 9.5
+ return 9.5 + (weight - 5) * 1.2
+
+def intl_base(weight):
+ if weight <= 1:
+ return 14.0
+ return 14.0 + (weight - 1) * 3.0
+
def shipping_cost(order):
- if order["country"] == "US":
- ...nested block...
- else:
- ...nested block...
+ if order["country"] == "US":
+ cost = us_base(order["weight"])
+ else:
+ cost = intl_base(order["weight"])
if order.get("express"):
cost = cost * 1.5
return round(cost, 2)
6. RISK NOTES
- Float order of operations must stay the same: express multiply before round. Caught by test_express_multiplies_after_base.
- Case-sensitive country check must stay. Caught by test_country_case_sensitive.
- KeyError on missing weight must stay. Caught by test_missing_weight_raises_keyerror.
Suspected bug, not fixed now: lowercase "us" is charged international.
7. API CHECK
round, dict.get: Python 3.11 standard library. us_base, intl_base, RATE_FUNCS: new in this plan. Nothing UNVERIFIED.