Module 7: Modernize Legacy Code
Module Project: Modernizing a Legacy Module
Module Project: Modernizing a Legacy Module
Project description
You're going to modernize a Python module with at least 5 tech debt items. Each change has a regression test and a separate commit. The deliverable is the modernized module + a detailed change log.
This project is directly part of Module 8 (Capstone Project) where modernization is one of the steps of the full migration of a legacy project.
Project Objective
Modernize a legacy module incrementally, preserving behavior with tests at each step.
By completing it:
- ✅ You'll have run a complete tech debt scan
- ✅ You'll have prioritized items with the impact/risk matrix
- ✅ You'll have modernized at least 5 tech debt items
- ✅ Each modernization has regression tests
- ✅ Each modernization has a separate commit
- ✅ A change log documents each change
Technical Specifications
Module to Modernize
Create this legacy module with intentional tech debt:
# src/legacy_module.py
import os
import sys
import json
from datetime import datetime, timedelta
CACHE = {}
API_URL = "https://old-api.example.com"
def get_data(type, id):
"""Get data from cache or API."""
key = "%s_%s" % (type, id)
if CACHE.get(key):
return CACHE[key]
try:
# Simulate API call
if type == "user":
data = {"id": id, "name": "User %d" % id, "type": type}
elif type == "product":
data = {"id": id, "name": "Product %d" % id, "price": id * 10.0}
else:
data = None
if data:
CACHE[key] = data
return data
except:
return None
def process_items(items):
results = []
for i in range(len(items)):
item = items[i]
if item.get("active") == True:
total = item["price"] * item["quantity"]
tax = total * 0.16
if item.get("region") == "EU":
tax = total * 0.21
elif item.get("region") == "UK":
tax = total * 0.20
final = total + tax
result = {
"name": item["name"],
"total": final,
"tax": tax,
"processed_at": str(datetime.now())
}
results.append(result)
return results
def save_to_file(data, filename):
path = os.path.join(os.getcwd(), "output", filename)
if not os.path.exists(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
f = open(path, "w")
f.write(json.dumps(data, indent=2))
f.close()
return path
def load_from_file(filename):
path = os.path.join(os.getcwd(), "output", filename)
if os.path.exists(path):
f = open(path, "r")
data = json.loads(f.read())
f.close()
return data
return None
def old_format_price(amount):
"""Deprecated: no longer used."""
return "$%.2f" % amount
def old_validate(data):
"""This function is never called anywhere."""
if data is None:
return False
if type(data) == dict:
return len(data) > 0
return True
class Config:
def __init__(self, host, port, debug, timeout):
self.host = host
self.port = port
self.debug = debug
self.timeout = timeout
def __repr__(self):
return "Config(host=%s, port=%s)" % (self.host, self.port)
def __eq__(self, other):
return (self.host == other.host and self.port == other.port
and self.debug == other.debug and self.timeout == other.timeout)
Tech Debt in This Module
- Dead code: unused
import sys,old_format_price(),old_validate(), unreferencedAPI_URL - %-formatting: 5+ instances of
%sand%d - bare except:
except:without specifying the exception - No context managers:
open()withoutwith - os.path: should be pathlib
- No type hints: no function has type hints
- Magic strings/numbers:
0.16,0.21,"EU","UK"hardcoded type() == X: should beisinstance()range(len()): should be direct iteration- Config class: a dataclass candidate
== True: unnecessary explicit comparison
Modernization Plan (5 minimum steps)
Step 1: Regression tests
> "Write regression tests for legacy_module.py:
get_data(), process_items(), save_to_file(),
load_from_file(), and Config. Run and confirm green."
Step 2: Dead code removal
Remove: import sys, old_format_price(), old_validate(), API_URL.
Step 3: Syntax modernization
%-formatting → f-strings, type() == X → isinstance(), range(len()) → direct iteration, == True → direct boolean.
Step 4: Pattern modernization
open() → context managers, os.path → pathlib, bare except → specific exceptions, magic numbers → constants/enum.
Step 5: Type hints + dataclass
Add type hints to public functions. Config → dataclass.
Deliverable
CHANGE_LOG.md
# Change Log: legacy_module.py Modernization
## Step 1: Regression Tests
- 12 tests written covering all the functions
- All green ✅
## Step 2: Dead Code Removal
- Removed: import sys, old_format_price(), old_validate(), API_URL
- Tests: ✅ (12/12 pass)
- Commit: "remove dead code from legacy_module"
## Step 3: Syntax Modernization
- Changed: 5x %-formatting → f-strings
- Changed: 2x type() → isinstance()
- Changed: 1x range(len()) → direct iteration
- Changed: 1x == True → boolean
- Tests: ✅ (12/12 pass)
- Commit: "modernize syntax in legacy_module"
## Step 4: Pattern Modernization
- Changed: 2x open() → with statement (pathlib)
- Changed: 1x bare except → except Exception
- Changed: os.path → pathlib throughout
- Added: TAX_RATES constant dict for magic numbers
- Tests: ✅ (12/12 pass)
- Commit: "modernize patterns in legacy_module"
## Step 5: Type Hints + Dataclass
- Added: type hints to all public functions
- Changed: Config class → @dataclass
- Tests: ✅ (12/12 pass)
- Commit: "add type hints and convert Config to dataclass"
## Metrics
| Metric | Before | After |
|--------|--------|-------|
| Lines | 95 | 78 |
| Dead code items | 4 | 0 |
| %-formatting | 5 | 0 |
| Type hints | 0% | 100% public |
| Context managers | 0 | 2 |
| Tech debt items | 11 | 0 |
Success Criteria
- ✅ The tech debt scan documents at least 5 items
- ✅ At least 5 modernizations executed
- ✅ Regression tests written BEFORE any change
- ✅ Tests pass after EACH step
- ✅ 1 commit per type of modernization
- ✅ CHANGE_LOG.md documents each change with metrics
Evaluation Rubric (100 points)
Tech Debt Scan (20 points)
- (10 pts) Complete inventory with types and severities
- (10 pts) Prioritization with justification
Modernization (40 points)
- (8 pts) Dead code removed
- (8 pts) Syntax modernized (f-strings, isinstance, etc.)
- (8 pts) Patterns modernized (context managers, pathlib)
- (8 pts) Type hints added
- (8 pts) At least 1 additional modernization (dataclass, enums)
Testing (25 points)
- (10 pts) Tests written BEFORE changes
- (10 pts) Tests pass after EACH step
- (5 pts) Coverage of the main functions
Documentation (15 points)
- (10 pts) CHANGE_LOG with detail per step
- (5 pts) Before/after metrics
Extra Credit (+10 points)
- (+3 pts) 7+ modernizations executed
- (+3 pts) Clean Git history with descriptive messages
- (+2 pts) Before/after readability comparison
- (+2 pts) All functions have docstrings
Common Errors
- Modernizing without tests first — without a safety net, you don't know if you broke something
- Mixing types of modernization — f-strings + type hints in the same commit
- Removing "dead code" that's actually used — verify with grep before removing
- Over-modernizing — not everything needs to be a dataclass or use the walrus operator
- Not documenting — the CHANGE_LOG is part of the deliverable
Resources for the Project
- pyupgrade - To verify your modernization
- vulture - To verify dead code
- mypy - To verify type hints
- pytest - For regression tests
- ruff - An ultra-fast linter for Python
- pathlib documentation - To replace os.path
What to Do if You Get Stuck?
If you don't know where to start:
→ Run the tech debt scan FIRST (capsule 02)
→ Without a scan, modernizing is subjective
→ The scan gives you the prioritization order
If the tests don't pass after step 2 (dead code):
→ Something in the "dead code" wasn't dead
→ Revert the last change
→ Verify with grep that the function really isn't used
→ If it's used dynamically, mark it as "candidate" not "dead"
If you're tempted to make all the changes together to "save time":
→ Read capsule 04 again
→ The total cost of big bang is 30-50% HIGHER, not lower
→ Resist the temptation
If Claude Code "finishes" the modernization in a single step:
→ Stop. Revert. Ask it explicitly: "only step N"
→ Don't accept the first output if it doesn't respect the incrementality
→ This is exercising trust calibration (guide #1 M5)
If CHANGE_LOG.md feels like bureaucracy:
→ It's the deliverable that demonstrates the process
→ Without it, you can't prove it was incremental
→ 5 minutes per step of documentation = 25 min total for 5 steps
Evidence of Success (Self-Verification)
Before declaring the project complete, validate that you meet these checkpoints:
Tech Debt Scan
- ✅ The inventory has at least 5 items with type, severity, and suggested fix
- ✅ Prioritization justified with the impact/risk matrix (capsule 02)
- ✅ Security items identified (if applicable) marked as critical/high
Tests
- ✅ The regression tests exist BEFORE the first change
- ✅ Each step ends with green tests — not with "I'll fix them later"
- ✅ The complete suite passes at the end of step 5
Modernization
- ✅ 5+ distinct types of modernization executed
- ✅ 5+ separate commits (one per type)
- ✅ Each commit has a descriptive message (not "WIP", "fix", "modernize")
Documentation
- ✅ CHANGE_LOG.md has a section for each step
- ✅ Each section documents: what was changed, why, resulting tests
- ✅ A before/after metrics table present and accurate
Quality
- ✅ The modernized module passes
pyupgrade --py310-pluswith no further changes - ✅ The modernized module passes
vulturewithout reporting dead code - ✅ All the public functions have type hints (if the project uses them)
If the 12 points are in place, the project demonstrates professional modernization. If you doubt any of them, go back to the corresponding capsule.
Connection with the Next Module
This project closes the modernization module. Module 8: Capstone Project takes all the techniques from the complete guide — onboarding, exploration, architecture analysis, refactoring, migration, context management, and modernization — and applies them in an end-to-end migration of a real legacy project.
What you did here (modernizing a module) is one step of the capstone project. There you're going to modernize 3-5 modules as part of a broader workflow that also includes architecture analysis, framework migration, and handoff documentation.