AI refactoring

noodlemind Beginner 4d ago 207 views 0 likes 4 min read

Automating your legacy Python debt with LLMs: A practical AI refactoring workflow

AI refactoring

I spent four hours last Thursday fighting a 400-line nested if-else block in a legacy data processing script. It was a nightmare of technical debt that no one wanted to touch because the regex patterns were "too sensitive." Instead of manual surgery, I decided to see if a local Llama 3 instance could handle the surgical extraction of logic into clean, testable functions.

Most people treat AI refactoring like a magic wand—you throw code at it and hope it doesn't break your production environment. That's a mistake. You need a structured pipeline to ensure the logic remains identical while the syntax improves.

Setting up the sandbox environment

You cannot experiment with AI refactoring on your main branch. You'll regret it. I use a Dockerized environment to isolate the LLM calls and the code execution to prevent the model from hallucinating a library that doesn't exist.

First, grab a lightweight container to run your testing suite. I prefer using a specific Python slim image to keep things fast.

docker run -it --name refactor-sandbox -v $(pwd):/app python:3.11-slim /bin/bash

Once inside, install pytest and rope (a Python refactoring tool). Rope is useful because it handles the AST (Abstract Syntax Tree) manipulations, which provides a safety net if the LLM suggests a syntax that is valid but structurally unsound.

pip install pytest rope langchain-community ollama

The prompt engineering mistake: Being too vague

The biggest reason AI refactoring fails is that users provide a prompt like "Clean this code up." That is useless. The model will change variable names to be "prettier" but might actually alter the side effects of a function.

To get actual value, you have to define the scope. I use a "Functional Extraction" template. I tell the model: "Extract the core logic of this block into a standalone function. Maintain all original error handling. Do not change the return type. Use type hinting."

Here is the specific prompt structure I feed into my local script:

Context: Legacy script 'data_parser.py' handles CSV cleaning.
Task: Refactor lines 45-88 into a function named 'sanitize_header'.
Constraint: Use the 're' module exactly as currently implemented. No new dependencies.
Output: Only the refactored function and its corresponding unit test.

If you are building complex Workflows to handle these transformations at scale, you should be looking at AST-based validation rather than just string matching.

Testing the refactored output

Let's look at a real-world transformation. I had this mess in a script:

AI refactoring

# Original legacy code (the mess)
def process(data):
if data:
parts = data.split(',')
if len(parts) > 1:
val = parts[1].strip()
if val.isdigit():
return int(val)
else:
return 0
return 0

After running my refactoring script with a GPT-4o API call, the output looked like this:

# Refactored version (the goal)
def get_integer_from_csv_part(data: str) -> int:
"""
Parses the second element of a comma-separated string.
Returns 0 if parsing fails or data is invalid.
"""
if not data:
return 0

parts = data.split(',')
if len(parts) <= 1:
return 0

val_str = parts[1].strip()
return int(val_str) if val_str.isdigit() else 0

The difference is readability. But how do we know it didn't break? I run a differential test. I compare the output of the old function against the new function using the same input set.

| Input String | Original Output | Refactored Output | Status |
| :--- | :--- | :--- | :--- |
| "test,123" | 123 | 123 | PASS |
| "test,abc" | 0 | 0 | PASS |
| "empty" | 0 | 0 | PASS |
| "test," | 0 | 0 | PASS |

Building a CLI tool for automated AI refactoring

I got tired of copy-pasting into a web UI, so I wrote a tiny Python utility to automate this. It reads a file, sends the specific lines to a local Ollama instance, and writes a .refactored version of the file.

import ollama

def automate_refactor(file_path, line_range):
with open(file_path, 'r') as f:
lines = f.readlines()

target_code = "".join(lines[line_range[0]:line_range[1]])

prompt = f"Refactor this Python code for clarity and type safety. Keep logic identical:\n\n{target_code}"

response = ollama.generate(model='llama3', prompt=prompt)
new_code = response['response']

with open(f"{file_path}.new", 'w') as f:
f.write(new_code)

Usage: automate_refactor('bad_script.py', (45, 88))

The wild part is that the model actually caught a bug I hadn't noticed. In the original code, parts[1] could theoretically throw an IndexError if the split worked strangely, but the refactored version included a check that made it more robust.

Avoiding the hallucination trap

One thing I've learned: LLMs love to suggest "modern" libraries. If you are working on an old system, you might not want to add pandas just to clean a single string.

When you are doing AI refactoring, you have to decide on your "Strictness Level."

1. Level 1 (Cosmetic): Just fix naming and PEP8.
2. Level 2 (Structural): Break long functions into smaller ones.
3. Level 3 (Logic Upgrade): Change the actual algorithm (risky).

I usually stick to Level 2. Anything more requires a human to verify the computational complexity. For example, if the AI replaces a simple loop with a complex list comprehension that runs in $O(n^2)$ instead of $O(n)$, you've actually made the performance worse despite the "cleaner" look.

Joining a community like PromptCube helps here because you realize most people aren't just asking ChatGPT questions; they are building entire pipelines that integrate these models into their IDEs and CI/CD loops. It's about moving from "chatting" to "engineering."

All Replies (0)

No replies yet — be the first!

Write a Reply

Markdown supported