Stop Refactoring Blind: 14 AI Prompts That Tame Legacy Python Without Breaking Production

Stop Refactoring Blind: 14 AI Prompts That Tame Legacy Python Without Breaking Production

Legacy Python has a nasty habit: it works in production, but every change feels like defusing a bomb. The code is 2,000 lines long, the tests are missing, and the only person who understood it left the company in 2021. You want to refactor, optimize, and maybe move to async — but you also want to keep the service up. That's where a well-crafted prompt changes everything.

This is a collection of 14 prompts that I've tested on real projects: a FastAPI endpoint that took 4 seconds per request, a Django module bloated to 2,400 lines, and a SQLAlchemy model drowning in N+1 queries. Each prompt includes what it's for, a usage example, and the expected outcome. No fluff — just copy, paste, and adapt.

A note on safety: never paste proprietary code into a public LLM without checking your company policy. Use self-hosted models or an AI agent that runs code in a sandbox when possible.

1. The Legacy Function Explainer

Task: You inherited a 300-line function with no docstring. Before touching it, you need to understand what it does, its side effects, and its edge cases.

Prompt:

You are a senior Python engineer. Analyze this function and explain in plain English:
1. What it does step by step.
2. Input/output types and assumptions.
3. Side effects (I/O, DB writes, global state).
4. Edge cases and potential bugs.
5. Suggested refactoring steps, ordered by risk (low to high).

Code:
<PASTE FUNCTION>

Example: I fed a process_order() function from a Django service. The model flagged that it mutated a global CACHE dict and swallowed exceptions silently — two bugs that had been causing phantom failures in production.

Expected result: A bullet-point breakdown plus a risk-ranked refactoring plan. You'll know exactly where to add tests before changing anything.

2. Split the 2,000-Line Module

Task: A single module has grown into a monster. You need a decomposition plan that doesn't break imports.

Prompt:

Here is a Python module (~2000 lines). Propose a decomposition into smaller modules by responsibility.
For each new module: name, purpose, public API, and which existing imports must be updated.
Keep backward compatibility: suggest re-exports in the original module.

Code:
<PASTE MODULE>

Example: A utils.py with 47 functions got split into text_utils.py, date_utils.py, and io_utils.py, with utils.py re-exporting everything so nothing broke.

Expected result: A file tree and a migration checklist. Zero broken imports if you follow the re-export advice.

3. Find Duplicated Logic Across Files

Task: The same validation logic appears in five places with subtle differences. You need to unify it.

Prompt:

Compare these files and identify duplicated logic. For each duplication:
- Show the common core.
- List the differences (and whether they are intentional).
- Propose a single reusable function or class.
- Warn about behavior changes if unified.

Files:
<PASTE 2-5 FILES>

Example: Email validation was duplicated in serializers.py, forms.py, and tasks.py. Two used regex, one used a library. The prompt caught a subtle difference: one allowed plus-addressing, the others didn't.

Expected result: A diff-aware consolidation plan. You avoid silently changing validation behavior.

4. Profile Before You Optimize

Task: You suspect a function is slow, but guessing is dangerous. Get a profiling plan.

Prompt:

Given this function, write a profiling script using cProfile and line_profiler.
Show how to run it and how to interpret the output.
Then suggest 3 optimization hypotheses ranked by expected impact.

Code:
<PASTE FUNCTION>

Example: cProfile revealed that 80% of time was in a regex compiled inside a loop. Moving it out cut response time by half.

Expected result: A runnable profiler script plus a ranked hypothesis list. You optimize what matters, not what looks ugly.

5. Convert Sync to Async Safely

Task: A FastAPI endpoint blocks the event loop with a sync DB call. You want async without a rewrite.

Prompt:

This FastAPI endpoint uses sync I/O. Rewrite it to async using asyncpg/httpx.
Keep the same response schema. Show:
- The async version.
- Which blocking calls were replaced.
- How to handle exceptions and timeouts.
- What tests to add.

Code:
<PASTE ENDPOINT>

Example: A requests.get call inside an endpoint was replaced with httpx.AsyncClient. Throughput on the endpoint improved from ~40 to ~300 req/s in a load test.

Expected result: An async version plus a test checklist. No event-loop blocking left.

6. Hunt N+1 Queries in SQLAlchemy

Task: Your list view fires hundreds of queries. You need to find and fix N+1.

Prompt:

This SQLAlchemy code likely has N+1 queries. Identify them and rewrite using selectinload/joinedload.
Show before/after query counts (estimated) and any trade-offs.

Code:
<PASTE QUERY LOGIC>

Example: A loop accessing order.customer.name triggered one query per order. Adding selectinload(Order.customer) reduced 500 queries to 2.

Expected result: Optimized ORM code with eager loading and a clear before/after query estimate.

7. Add Caching Without Stale Data Bugs

Task: A hot read path hits the DB on every request. You want caching but fear staleness.

Prompt:

Suggest a caching strategy for this function using Redis (or functools.lru_cache for in-process).
Cover: key design, TTL, invalidation on writes, and cache stampede protection.
Show code for both caching and invalidation.

Code:
<PASTE FUNCTION>

Example: A product catalog read went from ~120ms to ~4ms with Redis and a 60s TTL, plus explicit invalidation on product updates.

Expected result: A cache layer with a clear invalidation strategy. No "why is this data 10 minutes old" tickets.

8. Memory Leak Detective

Task: Your worker's memory grows until it's OOM-killed. You need a diagnostic plan.

Prompt:

This Python service leaks memory over time. Give a step-by-step plan to find the leak using tracemalloc, gc, and objgraph.
Include code snippets and what patterns to look for (unclosed resources, growing caches, circular refs).

Context:
<PASTE RELEVANT CODE + DESCRIPTION>

Example: tracemalloc snapshots showed a dict of session objects that never got evicted. Adding an LRU cache fixed it.

Expected result: A reproducible diagnostic routine and a list of likely culprits.

9. Type Hints for a Legacy Module

Task: You want mypy to catch bugs, but the module has zero annotations.

Prompt:

Add type hints to this Python module. Use Python 3.11+ syntax (list[str], X | None).
Annotate all public functions and classes. For ambiguous types, add a comment explaining your choice.
Then list places where mypy would likely complain and how to fix them.

Code:
<PASTE MODULE>

Example: Annotating a 600-line module surfaced three real bugs: a function returning None where callers assumed a list.

Expected result: A fully annotated module plus a mypy fix list. Static analysis becomes useful.

10. Write Tests Before Refactoring

Task: You can't refactor safely without a safety net. Generate characterization tests.

Prompt:

Write pytest tests that capture the current behavior of this function, including edge cases and known quirks.
Do not fix bugs — characterize them. Mark suspicious behavior with comments.

Code:
<PASTE FUNCTION>

Example: Tests captured that a parse_date function returned None on invalid input instead of raising. The quirk was documented, then fixed intentionally later.

Expected result: A passing test suite that locks in behavior. Now refactoring is safe.

11. Code Review Like a Senior

Task: You're reviewing a PR and want a second pair of eyes.

Prompt:

Review this Python diff as a senior engineer. Focus on: correctness, security, performance, and readability.
For each issue: severity (blocker/major/minor), explanation, and a concrete fix.
Do not nitpick style if a linter covers it.

Diff:
<PASTE DIFF>

Example: The review caught a missing await that would have silently returned a coroutine, plus a SQL injection risk in raw SQL.

Expected result: A prioritized review with actionable fixes. Fewer bugs reach production.

12. Dependency Upgrade Plan

Task: You're stuck on old libraries and need a safe upgrade path.

Prompt:

We use these dependencies: <LIST WITH VERSIONS>.
We want to upgrade to the latest stable versions.
Produce a step-by-step upgrade plan: order of upgrades, breaking changes to watch, and how to test each step.
Assume we have CI and a staging environment.

Example: Upgrading Django in three hops (3.2 → 4.2 → 5.x) avoided a giant, risky jump. Each step had its own test run.

Expected result: A phased upgrade roadmap. No "big bang" deploy.

13. Dead Code and Unused Imports Sweep

Task: The codebase carries years of unused functions and imports.

Prompt:

Analyze this module for dead code: unused functions, unreachable branches, and unused imports.
For each item, explain why you think it's dead and how to verify safely (grep for usages, check dynamic imports).

Code:
<PASTE MODULE>

Example: A legacy_export() function had zero callers — it was replaced years ago. Removing it cut 200 lines.

Expected result: A verified removal list. Smaller, clearer codebase.

14. Generate a Refactoring RFC

Task: You need buy-in from the team before a big refactor.

Prompt:

Write a short RFC for refactoring this module. Include: problem statement, goals, non-goals, proposed approach, risks, rollback plan, and success metrics.
Keep it under one page.

Context:
<PASTE DESCRIPTION + CODE SNIPPETS>

Example: The RFC convinced the team to allocate a sprint to split a monolith, with a clear rollback plan.

Expected result: A concise, reviewable document. Refactoring becomes a team decision, not a solo gamble.


Refactoring legacy Python isn't about rewriting everything. It's about understanding first, testing second, and changing third, in small, reversible steps. These 14 prompts turn vague anxiety into a concrete plan: profile before optimizing, test before refactoring, and always have a rollback. Start with the explainer and the characterization tests — they're the cheapest insurance you'll ever buy. Then pick one prompt that matches your current fire and run it on a real module today. Your future self (and your on-call rotation) will thank you.

← All posts

Comments