OpenAI Codex for Refactoring Large Functions Safely
Every codebase has one: the 400-line function nobody wants to touch
You know the one. It started as a clean 20-line handler, and three years, six engineers, and a dozen "quick fixes" later, it's a 400-line monster that validates input, talks to three different services, formats responses, logs telemetry, and handles five edge cases nobody remembers the reason for. Everyone is afraid to touch it. Every PR that goes near it gets an extra round of review. And every attempt to refactor it "properly" gets abandoned halfway because the blast radius feels too large.
This is where most refactoring advice falls apart. Books tell you to "extract methods" and "reduce cyclomatic complexity," but they don't tell you how to do that on a live function with real callers, undocumented side effects, and zero test coverage, without shipping a regression on a Friday afternoon.
OpenAI Codex — specifically the Codex CLI, run from your terminal against your actual repository — changes the economics of this problem. It's not that Codex has better taste in software design than you do. It's that it can read the entire function, trace every call site, hold the whole dependency graph in its context window, and mechanically apply a refactor in a way that's actually verifiable, step by step, instead of you doing it by hand across forty scroll positions in your editor.
This article is a practical playbook for using Codex to refactor large functions safely: how to scope the work, how to prompt it, how to verify each step, and how to avoid the failure modes that turn "AI refactor" into "AI regression."
Why large functions resist manual refactoring
Before getting into the Codex workflow, it's worth being precise about why large functions are hard to refactor by hand, because the reasons directly shape how you should use an AI coding agent to fix them.
- Working memory limits. A human reviewing a 400-line function has to hold the whole control flow in their head to safely extract a piece of it. Most people lose the thread after the third nested conditional.
- Hidden coupling. Large functions accumulate implicit dependencies — a variable set on line 12 that's read on line 340, a side effect buried in a helper call that three other things depend on.
- Missing tests. The functions most in need of refactoring are usually the least tested, because they grew organically and nobody wanted to write tests for "temporary" code that became permanent.
- Fear of regression. Without tests, every change is a bet. Engineers naturally minimize bets, which means they patch instead of restructure, which makes the function worse over time.
- No mechanical support. IDE "extract method" refactors handle the trivial cases. They can't reorganize a function's fundamental shape, split it into a pipeline, or reason about whether a piece of logic is safe to pull out because it doesn't have side effects.
Codex addresses the first two problems directly (it doesn't get tired holding context, and it can search the codebase for every reference to a variable or helper). It doesn't magically solve the missing-tests problem, but it can generate characterization tests fast enough that "no tests" stops being an excuse to leave the function alone.
Set up Codex CLI for a refactoring session
If you haven't used Codex CLI in your project yet, the setup is quick. It runs locally, operates directly on your files, and — critically for refactoring work — can execute your test suite and linter as part of its own loop, not just suggest code you paste in manually.
# Install the Codex CLI
npm install -g @openai/codex
# Authenticate (uses your OpenAI account / API key)
codex login
# From your project root, start an interactive session
codexOnce inside a session, Codex operates in your working directory with configurable permissions. For refactoring work, you want it to be able to read broadly across the repo (to trace callers) but you should be deliberate about write and execute permissions:
# Start a session scoped to read + edit, but ask before running shell commands
codex --ask-for-approval on-request
# Or, if you want it to run tests automatically after edits
codex --ask-for-approval on-failure --sandbox workspace-writeThe on-failure approval mode is a good default for refactoring sessions: Codex can freely read files, make edits, and run your test suite, but it stops and asks before doing anything destructive or before running a command it can't recover from cleanly. This gives you a tight feedback loop without babysitting every single file write.
Step 1: Don't refactor blind — generate characterization tests first
The single biggest safety upgrade you can make before touching a large function is to have Codex write characterization tests: tests that capture *current* behavior, not desired behavior. You're not testing that the function is correct. You're testing that after refactoring, it does exactly what it did before.
Prompt Codex like this:
This function has no test coverage. Before we refactor anything, write
characterization tests that capture its current behavior, including
edge cases and any weird-looking special casing. Do not "fix" anything
you think looks like a bug — just document current behavior with tests.
Cover:
- normal inputs
- boundary values (empty, null, zero, max-length)
- any branch that looks unusual or undocumented
- error paths and what exceptions/return values they produce
Put tests in tests/test_process_order.py and run them to confirm they
pass against the current implementation before we change anything.This is a critical discipline: tell Codex explicitly not to "fix" anything yet. Left unguided, a model will often notice something that looks like a bug and quietly correct it while writing tests, which defeats the purpose — you need a baseline of the function's *actual* behavior, bugs included, so you can refactor without changing behavior. Bug fixes come later, as a separate, deliberate step.
Here's a simplified example of the kind of function this applies to, and what characterization tests around it might look like:
def process_order(order, inventory, user, config):
if not order or not order.get("items"):
return {"status": "error", "code": "EMPTY_ORDER"}
total = 0
discounted_items = []
for item in order["items"]:
sku = item.get("sku")
qty = item.get("qty", 1)
if sku not in inventory:
return {"status": "error", "code": "UNKNOWN_SKU", "sku": sku}
stock = inventory[sku]["stock"]
if stock < qty:
return {"status": "error", "code": "INSUFFICIENT_STOCK", "sku": sku}
price = inventory[sku]["price"]
if user.get("tier") == "gold" and qty >= 3:
price = price * 0.9
discounted_items.append(sku)
elif config.get("promo_active") and sku in config.get("promo_skus", []):
price = price * 0.85
discounted_items.append(sku)
total += price * qty
inventory[sku]["stock"] -= qty
if user.get("tier") == "gold":
total = total * 0.95 if total > 100 else total
tax = total * config.get("tax_rate", 0.0)
shipping = 0 if total > config.get("free_shipping_threshold", 999999) else config.get("shipping_cost", 5)
return {
"status": "ok",
"total": round(total + tax + shipping, 2),
"discounts_applied": discounted_items,
"tax": round(tax, 2),
"shipping": shipping,
}# tests/test_process_order.py
def test_gold_tier_bulk_discount_stacks_with_order_discount():
order = {"items": [{"sku": "A1", "qty": 3}]}
inventory = {"A1": {"stock": 10, "price": 50}}
user = {"tier": "gold"}
config = {"tax_rate": 0.1, "free_shipping_threshold": 100, "shipping_cost": 5}
result = process_order(order, inventory, user, config)
# documents CURRENT behavior: gold bulk discount (10%) AND the
# gold >100 discount (5%) both apply, compounding
assert result["status"] == "ok"
assert result["discounts_applied"] == ["A1"]
assert result["total"] == 141.53 # captured, not designed
def test_promo_and_gold_bulk_discount_are_mutually_exclusive():
# documents that gold-tier bulk discount takes priority over promo
# discount even when both conditions are true — this may or may not
# be intentional, but it IS current behavior
order = {"items": [{"sku": "B2", "qty": 3}]}
inventory = {"B2": {"stock": 10, "price": 40}}
user = {"tier": "gold"}
config = {"promo_active": True, "promo_skus": ["B2"], "tax_rate": 0.0}
result = process_order(order, inventory, user, config)
assert result["total"] == round(3 * 40 * 0.9, 2)Notice the comments calling out surprising behavior explicitly. This is Codex doing valuable analysis work: it read the function closely enough to notice the two discount branches are mutually exclusive (an if/elif, not independent checks) and that gold-tier discounts can compound. That's exactly the kind of subtlety that gets silently lost in a manual refactor.
Step 2: Ask Codex to map the function before changing it
Once you have a safety net, don't jump straight to "refactor this." Ask Codex for a structural map first. This forces it (and you) to articulate the function's actual responsibilities before deciding how to split them.
Analyze process_order() and produce a breakdown of its distinct
responsibilities as a list, in the order they execute. For each
responsibility, note:
1. what it depends on (inputs, shared mutable state)
2. what it produces or mutates
3. whether it has side effects (e.g., mutating `inventory` in place)
4. whether it could be extracted as a pure function
Don't write any code yet — just the analysis.A good response identifies something like:
- Input validation (pure, no dependencies beyond
order) - Inventory/stock validation per item (reads
inventory, no mutation, pure) - Price calculation with discount rules (reads
user,config; pure, but has two discount branches worth naming explicitly) - Inventory mutation (side effect: decrements
inventory[sku]["stock"]— this is the one that must stay carefully sequenced relative to validation) - Order-level gold discount (pure, depends on
totalanduser) - Tax and shipping calculation (pure, depends on
configandtotal) - Response assembly (pure)
This map is the actual refactoring plan. Notice it isolates the one piece of real state mutation (decrementing stock) from everything else, which is exactly the piece you need to be most careful about when splitting the function — pure calculations can be extracted and reordered fairly freely, but the stock mutation has to happen exactly once, at the right point in the loop, relative to the validation that precedes it.
Step 3: Refactor in small, independently verifiable steps
This is the part where most manual refactors and most naive "just rewrite it" AI prompts both fail. The fix is the same for both: don't do the whole refactor in one shot. Have Codex do it as a sequence of small, independently testable transformations, running your characterization tests after each one.
Refactor process_order() using the responsibility map from before.
Do it as a sequence of small commits, running the full test suite
after each one:
1. Extract input validation into `validate_order(order)`, returning
an error dict or None. No behavior change.
2. Extract stock validation into `check_stock(order, inventory)`.
No behavior change.
3. Extract price calculation (including both discount branches,
preserved exactly as-is) into `price_line_item(item, inventory,
user, config)`. No behavior change.
4. Extract the stock-decrement mutation into `apply_stock_deduction(
order, inventory)`, called once, in the same relative position
as before.
5. Extract tax/shipping into `calculate_totals(total, config)`.
No behavior change.
6. Reassemble process_order() as a thin orchestrator calling the
above in order.
After each step, run `pytest tests/test_process_order.py -v` and
show me the output before moving to the next step. Stop and flag me
if any test fails — do not "fix" the test to match new behavior.That last line matters more than it looks. It's a common failure mode: a model changes behavior, a characterization test fails, and instead of stopping, it "fixes" the test to match the new (wrong) output. Explicitly forbidding that turns your test suite from decoration into an actual safety net.
Here's roughly what the orchestrator looks like after this sequence completes:
def process_order(order, inventory, user, config):
error = validate_order(order)
if error:
return error
stock_error = check_stock(order, inventory)
if stock_error:
return stock_error
total = 0
discounted_items = []
for item in order["items"]:
price, discounted = price_line_item(item, inventory, user, config)
if discounted:
discounted_items.append(item["sku"])
total += price * item.get("qty", 1)
apply_stock_deduction(order, inventory)
if user.get("tier") == "gold":
total = total * 0.95 if total > 100 else total
tax, shipping = calculate_totals(total, config)
return {
"status": "ok",
"total": round(total + tax + shipping, 2),
"discounts_applied": discounted_items,
"tax": round(tax, 2),
"shipping": shipping,
}Every extracted function is independently testable, independently readable, and — this is the real payoff — independently reviewable. A reviewer can look at price_line_item in isolation and actually reason about the discount logic instead of holding the whole 400-line function in their head.
Step 4: Use Codex to check for behavior-preserving guarantees, not just passing tests
Passing tests are necessary but not sufficient. Characterization tests only cover the inputs you thought to test. For a function this central, ask Codex to reason explicitly about equivalence between the old and new code, not just rely on the test suite.
Compare the original process_order() (see git history / diff) against
the refactored version. Walk through every branch and confirm, line
by line, that the new version produces identical output for identical
input across all code paths — including error paths, the two discount
branches, and the order of operations around inventory mutation.
Call out anything where behavior could differ even subtly, e.g.:
- floating point rounding done in a different order
- short-circuit evaluation changes
- the stock deduction happening before/after a validation check that
used to run in a different orderThis step catches the class of bugs that tests miss because nobody thought to write a test for them — often reordering bugs, where extracting code changes *when* something happens relative to something else. Codex is well suited to this because it's comparing two concrete pieces of text against each other rather than relying on intuition about "the general shape" of the function.
Step 5: Let Codex clean up call sites and update callers
A large function extraction often exposes further opportunities: maybe three other places in the codebase have near-duplicate logic for stock validation, or now that price_line_item is a standalone function, it should be reused elsewhere instead of a copy-pasted version.
Search the codebase for other places that duplicate logic similar to
check_stock() or price_line_item() (things like inline stock checks
or discount calculations for gold-tier users). List every match with
file and line number, and for each one tell me whether it's safe to
replace with a call to the new extracted function, or whether it has
subtly different behavior that needs to be preserved.This is where Codex's ability to search across an entire repository pays off — this kind of duplicate-logic hunt is exactly the tedious, error-prone task that gets skipped in manual refactors because nobody has time to grep the whole codebase and read each match carefully.
Guardrails: what to never let Codex do unsupervised in a refactor
A few hard rules keep this workflow safe, based on where AI-assisted refactors tend to go wrong in practice:
- Never let it modify tests and implementation in the same step when the goal is behavior preservation. If a test needs to change, that's a decision you make explicitly, not something folded into a "fix" commit.
- Never skip the characterization-test step for functions with side effects (database writes, external API calls, mutation of shared state). The riskier the function, the more valuable the tests you write before touching it.
- Never approve a refactor PR from a diff summary alone. Read the actual diff. Codex is good at mechanical transformation, but you are the one accountable for correctness in production.
- Always run the full test suite, not just the tests touching the function. Extracted helpers sometimes get reused or imported in ways that touch other modules.
- Keep commits small and revertable. Each extraction step should be its own commit so a bad step can be reverted without losing the good ones.
A repeatable checklist for your next big-function refactor
- 1. Identify the function and confirm it has no adequate test coverage.
- 2. Ask Codex to write characterization tests capturing current behavior, explicitly including odd edge cases, without "fixing" anything.
- 3. Run the tests and confirm they pass against the unmodified function.
- 4. Ask Codex for a responsibility map: what the function does, in order, noting side effects and mutable state.
- 5. Refactor in small steps, one responsibility extracted per step, running tests after each.
- 6. Instruct Codex to stop and flag failures rather than adjusting tests to match new behavior.
- 7. Ask for an explicit line-by-line equivalence check between old and new code, focused on ordering, rounding, and short-circuit branches.
- 8. Search the codebase for duplicated logic that the new extracted functions could replace.
- 9. Review the actual diff yourself before merging — don't rely on a summary.
- 10. Keep each extraction as its own commit so any single step can be reverted independently.
Closing thoughts
Large, feared functions are one of the clearest wins for AI-assisted engineering, precisely because the hard part was never typing out the extracted function — it was holding the whole thing in your head long enough to be confident nothing broke. Codex doesn't replace the judgment calls (whether a discount should compound, whether two branches are actually meant to be different), but it removes the friction that made careful refactoring too expensive to attempt: writing characterization tests for untested code, tracing every call site across a large repo, and checking line-by-line equivalence between two versions of a function.
Used with the discipline outlined here — tests before changes, small steps, explicit stop conditions, and a human reviewing the final diff — Codex turns "nobody wants to touch that function" into a routine, low-risk cleanup task instead of a standing item on the tech-debt backlog that never gets picked up.
If you want to go deeper on this workflow with hands-on exercises, our OpenAI Codex CLI Tutorial course on teachyou.ai walks through setting up Codex CLI, configuring sandbox and approval modes for different risk levels, and applying this exact refactoring methodology to real, messy codebases step by step.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
CodexLearn to drive OpenAI's coding agent: real tasks, safe sandboxing, and terminal-to-cloud workflows that ship.