Developer Guide
How to Safely Refactor Python Code You Didn't Write
Every developer has inherited code they didn't write. A service built by someone who left the company. A module that's been running in production since 2019 with no tests and no documentation. A function that everyone on the team is afraid to touch because nobody fully understands what it does.
This is not an edge case. It's the normal condition of software development at any company older than three years.
Refactoring code you wrote is hard enough. Refactoring code you didn't write — code whose full behavior you can't be certain of — is a different problem entirely. This post is about how to approach it without breaking things.
Why inherited code is different
When you refactor code you wrote, you have implicit knowledge that doesn't exist in the file. You remember why you made that decision in 2022. You know which edge cases that conditional is handling. You know the function that looks unused actually gets called from a config file.
With inherited code, that implicit knowledge is gone. What's left is the code itself, whatever tests exist (often fewer than you'd like), and whatever documentation exists (often none).
This changes the risk profile of refactoring significantly. A change that looks safe based on reading the code might break a behavior that only exists in the original author's head — or in a production edge case that the tests don't cover.
The wrong approach: rewrite it
The most common response to inheriting a messy codebase is the impulse to rewrite it. Start fresh, do it properly, fix everything at once.
This almost always makes things worse.
Joel Spolsky called this the single worst strategic mistake a software company can make, and the reasoning holds at the module level too. The existing code, messy as it is, encodes years of bug fixes, edge case handling, and implicit requirements. A rewrite throws all of that away. What you get back is cleaner code that breaks in ways the original code didn't — because the original code had already encountered and handled those situations.
The right approach is incremental.
A practical framework for inherited code
Step 1: Understand before you touch.
Run static analysis before making any changes. You want a map of the codebase — where the complexity is concentrated, what the dependencies look like, where the security risks are. This is orientation, not action.
refactron analyze .
Read-only. Nothing changes. You get a prioritized list of issues with file names, line numbers, and severity levels. Now you know what you're dealing with.
Step 2: Start with the safe, obvious changes.
Unused imports, dead code, redundant variable assignments, obvious naming issues — these are low-risk, high-readability improvements that don't change behavior. They make the code easier to read while you're learning it. They're also the easiest to verify as safe.
refactron autofix . --verify
Every change passes three checks before it touches a file. If anything fails, the original is untouched. For low-risk changes like import cleanup, these checks run in under a second and pass reliably.
Step 3: Work outward from the tests.
Before touching anything structural, understand your test coverage. If coverage is low, the first real investment is writing tests for the behavior you're about to change — not the refactoring itself. Tests are what make structural changes safe. Without them, you're flying blind regardless of what tools you use.
If the code has no tests at all — which is common in older internal services — start by writing characterization tests: tests that describe what the code currently does, not what you think it should do. These become your verification baseline.
Step 4: Make one structural change at a time.
The temptation is to clean everything at once. Resist it. One function, one module, one change at a time. Run verification after each one. Commit after each one. Build a record of what changed and why.
This feels slow. It's not. The alternative — large refactoring sessions that produce large diffs that are hard to review and hard to roll back — is what creates incidents.
Step 5: Document as you go.
Every time you understand why a piece of code does something non-obvious, write it down. A comment, a docstring, a README section. The next person to touch this code — which might be you in six months — will thank you. This is how tribal knowledge stops being tribal.
The specific risks to watch for
A few failure modes come up repeatedly when refactoring inherited Python code.
Removing imports that look unused but aren't. Python's import system has side effects. An import that doesn't appear to be referenced in a file might be registering a plugin, configuring a logger, or loading a Django app. Removing it breaks things in ways that don't show up until runtime. Always check whether an import has side effects before treating it as dead code.
Renaming functions that are called by strings. If a function is called via getattr(), a config file, or a serialized reference somewhere, renaming it in the code doesn't rename it everywhere it's referenced. The code looks fine, the tests pass, it breaks in production when a config value calls a function that no longer exists by that name.
Changing default parameter values. Default parameters in Python are evaluated once at function definition, not at call time. Changing a default can change behavior in ways that are subtle and surprising, especially for mutable defaults like lists or dictionaries.
Simplifying conditionals that encode business logic. A conditional that looks like it's testing the same thing twice might be testing two subtly different things that happen to look the same. Before simplifying, understand what each branch is actually doing.
The mindset shift
Refactoring inherited code safely is less about the tools and more about the mindset. The goal isn't a clean codebase — it's a codebase that works now and is slightly easier to work with than it was before. Progress, not perfection.
Every small, safe, verified improvement compounds. Three months of incremental cleanup looks completely different from three months of deferred cleanup — not because any individual change was dramatic, but because they accumulate.
The code you inherited didn't get messy overnight. It won't get clean overnight either. The teams that manage this best are the ones who make safe incremental improvement a habit, not a project.
pip install refactron refactron analyze .
Start with understanding. Nothing changes until you ask it to.