Module 7: Modernize Legacy Code

Identifying Tech Debt with Claude Code

Identifying Tech Debt with Claude Code

Capsule description

Before modernizing, you need an inventory: what tech debt does this code have? Claude Code can scan a complete module and list code smells, deprecated patterns, dead code, unused imports, and missing type hints — in minutes. Manually, this audit takes hours.

In this capsule you're going to learn to run systematic tech debt scans with Claude Code, categorize findings by type, and prioritize them with the impact/risk matrix.


The 6 Types of Tech Debt

1. Deprecated Syntax

# Old Python:
name = "Hello, %s" % user_name           # %-formatting
items = dict([(k, v) for k, v in data])   # verbose dict comprehension
if type(x) == int:                         # type() comparison
file = open("data.txt")                    # no context manager

# Modern Python:
name = f"Hello, {user_name}"              # f-strings
items = {k: v for k, v in data}           # dict comprehension
if isinstance(x, int):                     # isinstance()
with open("data.txt") as file:            # context manager

2. Missing Type Hints

# Without type hints (ambiguous):
def calculate_total(items, discount, tax_rate):
    subtotal = sum(i["price"] * i["qty"] for i in items)
    return subtotal * (1 - discount) * (1 + tax_rate)

# With type hints (clear):
def calculate_total(
    items: list[dict[str, float]],
    discount: float,
    tax_rate: float
) -> float:
    subtotal = sum(i["price"] * i["qty"] for i in items)
    return subtotal * (1 - discount) * (1 + tax_rate)

3. Dead Code

import os           # never used
import json         # never used
from datetime import timedelta  # never used

def old_calculate_tax(amount):  # never called
    """Deprecated: use calculate_tax_v2"""
    return amount * 0.16

LEGACY_URL = "https://old-api.example.com"  # never referenced

4. Deprecated Patterns

# Old pattern: manual error handling
try:
    file = open("config.json")
    data = json.load(file)
    file.close()
except:                          # bare except (catches everything)
    pass                         # silences errors

# Modern pattern: context manager + specific exception
try:
    with open("config.json") as file:
        data = json.load(file)
except FileNotFoundError:
    data = {}
except json.JSONDecodeError as e:
    logger.error(f"Invalid config: {e}")
    data = {}

5. Code Duplication

# In user_service.py:
tax = subtotal * 0.16
if region == "EU":
    tax = subtotal * 0.21

# In order_service.py (identical):
tax = subtotal * 0.16
if region == "EU":
    tax = subtotal * 0.21

# In invoice_service.py (identical):
tax = subtotal * 0.16
if region == "EU":
    tax = subtotal * 0.21

6. Deprecated Dependencies

# requirements.txt with deprecated deps:
flask==1.1.4        # EOL, should be 3.x
requests==2.25.0    # old, use httpx or update
python-jose==3.3.0  # unmaintained, use PyJWT

Systematic Scan with Claude Code

The tech debt scan prompt

> "Analyze [file or module] and generate a complete
   tech debt inventory. For each item, report:
   1. Type (syntax, type hints, dead code, pattern, duplication, dependency)
   2. Location (file:line)
   3. Severity (high/medium/low)
   4. Description (what it is and why it's tech debt)
   5. Suggested fix (how to modernize it)
   
   Organize by type and severity."

Expected output

# Tech Debt Inventory: src/services/order_service.py

## Deprecated Syntax (3 items)
| # | Line | Severity | Description | Fix |
|---|-------|-----------|-------------|-----|
| 1 | 23 | Low | %-formatting | f-string |
| 2 | 45 | Low | dict() with a list comprehension | dict comprehension |
| 3 | 67 | Medium | open() without a context manager | with statement |

## Missing Type Hints (5 items)
| # | Line | Severity | Description | Fix |
|---|-------|-----------|-------------|-----|
| 1 | 12 | Medium | create_order() without type hints | Add hints |
| 2 | 34 | Medium | calculate_total() without hints | Add hints |
| ... | ... | ... | ... | ... |

## Dead Code (2 items)
| # | Line | Severity | Description | Fix |
|---|-------|-----------|-------------|-----|
| 1 | 5 | Low | import os (unused) | Remove |
| 2 | 89 | Medium | old_validate() never called | Remove |

## Deprecated Patterns (2 items)
| # | Line | Severity | Description | Fix |
|---|-------|-----------|-------------|-----|
| 1 | 67 | Medium | bare except | Specific exceptions |
| 2 | 78 | High | SQL string concatenation | Parameterized query |

## Total: 12 items (2 high, 5 medium, 5 low)

Prioritization with the Impact/Risk Matrix

The matrix

High impactLow impact
Low risk✅ FIRST🔄 When convenient
High risk⚠️ Plan❌ Probably not worth it

Applying the matrix

✅ FIRST (high impact, low risk):
  - Dead code removal
  - Import cleanup
  - bare except → specific exceptions

🔄 WHEN CONVENIENT (low impact, low risk):
  - %-formatting → f-strings
  - verbose dict() → dict comprehension

⚠️ PLAN (high impact, high risk):
  - SQL concatenation → parameterized (security)
  - Missing type hints on public functions

❌ PROBABLY NOT (low impact, high risk):
  - Rewriting functions that work "for aesthetics"

Connection with the Project

In the Module Project (capsule 05), the first step is a complete tech debt scan. The prioritization determines the modernization order.


Troubleshooting

Problem 1: Claude Code reports too many items

Solution: Filter by severity. Focus on high and medium. The low ones are "nice to have."

Problem 2: I don't know if something is really tech debt

Solution: Ask: "Would this code cause problems in a 2026 code review?" If yes, it's tech debt. If it works well and is readable, it may be fine.

Problem 3: The team doesn't use type hints

Solution: Don't introduce type hints in a module if the rest of the project doesn't use them. Modernization should be aligned with the team.


Exercises

Exercise 1: Classify tech debt (Easy)

Classify each item by type and severity:

  1. import sys that's never used
  2. except: without specifying the exception
  3. A 300-line function with 5 responsibilities
  4. "Hello %s" % name instead of an f-string
  5. SQL query with string concatenation: f"SELECT * FROM users WHERE id = {user_id}"
See solution
  1. Dead code, Low — doesn't affect functionality
  2. Deprecated pattern, Medium — can hide errors
  3. Code smell, High — hard to maintain and test
  4. Deprecated syntax, Low — functional, only cosmetic
  5. Security vulnerability, VERY HIGH — SQL injection possible

Exercise 2: Write a scan prompt (Medium)

Write the prompt for Claude Code to scan all of src/services/ looking only for deprecated patterns and dead code.

See solution
> "Scan all the files in src/services/ looking for:
   1. Dead code: functions never called, unused imports,
      unreferenced variables
   2. Deprecated patterns: bare except, open() without with,
      manual file.close(), %-formatting, type() comparison
   
   For each finding, report: file, line, type,
   and suggested fix. Ignore type hints and code style."

Common Errors in Tech Debt Scans

Error 1: Confusing "I don't like it" with "tech debt"

Symptom: Your list includes "this name is ugly" or "I'd write it differently". The team rejects the PR.

Why it happens: Aesthetic preferences sneak in as "tech debt". But tech debt is objective cost: future bugs, maintenance difficulty, security debt — not personal style.

How to fix: For each item, ask yourself: "Would this code cause problems in a professional code review, or does it just bother me?". If it just bothers you, it's not debt — it's preference.

Error 2: Uniform severity ("everything is important")

Symptom: 30 items, 30 marked as medium-high severity. Prioritization is impossible.

Why it happens: Each item feels important at the moment. But prioritizing requires differentiating — and that means marking most as low.

How to fix: A typical healthy distribution: 10% high, 30% medium, 60% low. If everything is high, nothing is high. SQL injection is high. F-string vs %-formatting is low, not medium.

Error 3: Marking "dead code" without dynamic verification

Symptom: You removed 3 "dead" functions. In production, one is called from a cron job. Incident.

Why it happens: Blind trust in grep. Python has dynamic callbacks, decorators with strings, getattr, plugins, entry points in setup.py.

How to fix: Before marking as dead, verify:

  1. grep -r "function_name" in the WHOLE repo (not just the module)
  2. Search in strings: grep -r '"function_name"'
  3. Look in config files (YAML, TOML, JSON)
  4. If logs are accessible, verify it doesn't appear in the last 30 days
  5. If you still doubt, mark it as "candidate for removal" — not removed yet

Error 4: Looking for tech debt without understanding the project context

Symptom: You marked "doesn't use async" as tech debt in a sync-first project. The team laughs.

Why it happens: You applied a "modern" pattern without checking whether it fits the project. Not every project needs to be async, type-hinted, or dataclass-based.

How to fix: Before the scan, read CLAUDE.md or the README. What patterns does the project use? Modernization should align with the project, not impose a foreign style.

Error 5: Not including security in the scan

Symptom: Your list has 30 cosmetic items but doesn't detect SQL injection, hardcoded secrets, or missing validation.

Why it happens: The instinct is to look for "old code". But the worst tech debt is security debt — and it looks more subtle than an f-string.

How to fix: Explicitly include in your prompt: "Also identify: SQL string concatenation, hardcoded secrets/passwords/keys, missing validation on external inputs, error handling that leaks sensitive info". Default severity: high or critical.


Summary

  • 6 types of tech debt: syntax, type hints, dead code, patterns, duplication, dependencies
  • Claude Code scans in minutes what manually takes hours
  • Prioritize with impact/risk: dead code first, cosmetic later
  • Tech debt isn't negligence — it's natural evolution
  • The scan is the input for the modernization (capsules 03-04)
  • Tell preference apart from debt — not everything that bothers you is debt
  • Dynamically verify dead code before marking it
  • Include security explicitly in the scan

Next capsule: Modernizing Deprecated Syntax and Patterns — executing the fixes.


Additional Resources

  1. pylint - A linter that detects many code smells
  2. vulture - A dead code finder for Python
  3. pyupgrade - Automatic syntax modernization
  4. bandit - A security scanner for Python
  5. Refactoring Guru - Code Smells - A complete catalog
  6. SonarQube - A code quality analysis platform

Module 7, Capsule 02 — Refactoring & Legacy Code with Claude Code Guide