Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
How-tos

How to Refactor Legacy Python Code Safely

Learn a disciplined, step-by-step process for refactoring legacy Python code without breaking it. Write characterization tests, make one change at a time, and know when to stop and ship.

July 2026 6 min read 2 views 0 hearts

You stare at that old Python file. The one nobody wants to touch. The one with functions that stretch across three screens, variables named temp1, temp2, and x, and comments like "# TODO: fix this mess" from 2014.

It's scary. But you can do it safely. Here's how.

Start with Tests (Even Crappy Ones)

Before you change a single line, you need a safety net. The problem is legacy code often has zero tests. So write characterization tests.

# Old function you need to refactor
def process_data(a, b, c, d, e):
    # 200 lines of spaghetti
    return result

# Write this first
def test_process_data_keeps_old_behavior():
    # Call with specific inputs
    result = process_data(1, 2, 3, 4, 5)
    assert result == 15  # Run it once to know what it actually returns

    # Add a few more test cases based on real usage
    assert process_data(0, 0, 0, 0, 0) == 0
    assert process_data(-1, -2, -3, -4, -5) == -15

The tests don't need to be perfect. They just need to document what the code actually does today. Run them. If they pass, you have a baseline.

The Golden Rule: One Change at a Time

Here is where most people mess up. They try to rename variables, extract functions, fix bugs, and improve performance all at once. Don't.

Pick one thing: - Extract a single function - Rename one variable - Remove one dead code block

Make that change. Run tests. If they pass, commit. If they fail, revert.

I once watched a PythonSkillset team member spend three days untangling a single function that had been "slightly improved" by five different people over two years. The function did one thing badly. But it worked.

The Extract Pattern That Saves You

Legacy functions usually do too many things. Find the natural seams.

# Before
def save_user(name, email, age, address, phone):
    # Validate name
    if not name or len(name) > 100:
        return False

    # Validate email
    if '@' not in email:
        return False

    # Validate age
    if age < 0 or age > 150:
        return False

    # Save to database
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute("INSERT INTO users ...")
    conn.commit()
    return True

# After (step by step)
def is_valid_name(name):
    return name and len(name) <= 100

def is_valid_email(email):
    return '@' in email

def is_valid_age(age):
    return 0 <= age <= 150

def save_user(name, email, age, address, phone):
    if not is_valid_name(name):
        return False
    if not is_valid_email(email):
        return False
    if not is_valid_age(age):
        return False

    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute("INSERT INTO users ...")
    conn.commit()
    return True

Extract one validation at a time. Test after each extraction. This is boring. It is also how you avoid breaking everything.

The Dangerous Three: Global State, Side Effects, and Mutable Defaults

Legacy Python code loves these. They are landmines.

# Dangerous
def add_item(item, cart=[]):
    cart.append(item)
    return cart

# Safer
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

Same for global variables. If you see global in a function, your refactoring just got harder. You have three options: 1. Pass it as a parameter 2. Create a class 3. Accept the pain and move slowly

Option 1 is usually best. Option 3 is honest.

When To Stop and Ship

You do not have to refactor everything. Seriously.

Here is a real example from PythonSkillset's own codebase. There was a module that processed CSV files. It was 900 lines long, had no tests, and used eval() in two places. The team spent two weeks refactoring it. A month later, the business stopped using CSV files entirely.

What should they have done? Written tests around the eval() parts (to understand them), extracted the CSV reading logic (because that was stable), and left the rest alone until a feature change forced the issue.

The Pattern for Safely Refactoring Legacy Code

Here is your cheat sheet:

  1. Write one test that captures current behavior (even if it feels stupid)
  2. Pick the smallest possible change you can make
  3. Make the change
  4. Run tests
  5. If tests pass, commit. If they fail, revert.
  6. Repeat until you run out of time or motivation
  7. Ship what you have

No magic. No "automated refactoring tools will save you." Just disciplined, boring, step-by-step improvement.

The code will still be ugly. But it will be less ugly than when you started. And it will still work.

That is the win.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.