Python · Course navigation

Refactoring safely with tests

Restructure code without changing its behavior, and prove that behavior is unchanged.

On this page
Before you start

Conditionals, functions, and unit testing.

  • Restructure code without changing its behavior, and prove that behavior is unchanged.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

Refactoring means changing the internal structure of code to make it easier to understand and change, without changing what it does. It is done in small steps, and after each step you check that behavior is still the same. Without checks, refactoring turns into risky rewriting.

The safety net is tests. When a function has no tests, write characterization tests first: record what it currently returns for a representative set of inputs, including boundaries, then restructure. Here the original shipping function is a pyramid of nested conditions that repeats the weight limits. The new version expresses the same rules as a small table that is easy to read and extend.

The program compares the old and the new function on every combination of weight and shipping speed in a table of cases that includes the boundary weights 1 and 5. If any answer differs, the assertion fails. Notice that refactoring did not add a feature: when you find a bug or want new behavior, do that as a separate change so each change is easy to review.

Code example

Python
def shipping_before(weight, express):
    if express == True:
        if weight <= 1:
            return 15
        else:
            if weight <= 5:
                return 25
            else:
                return 40
    else:
        if weight <= 1:
            return 5
        else:
            if weight <= 5:
                return 10
            else:
                return 20


RATES = [(1, 5, 15), (5, 10, 25)]


def shipping_after(weight, express):
    for limit, standard, fast in RATES:
        if weight <= limit:
            return fast if express else standard
    return 40 if express else 20


cases = [(w, e) for w in (0, 1, 1.5, 5, 5.1, 50) for e in (False, True)]
assert all(shipping_before(w, e) == shipping_after(w, e) for w, e in cases)
print(len(cases), "cases identical")
print(shipping_after(3, True), shipping_after(30, False))
Output
12 cases identical
25 20

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1def shipping_before(weight, express):
2 if express == True:
3 if weight <= 1:
4 return 15
5 else:
6 if weight <= 5:
7 return 25
8 else:
9 return 40
10 else:
11 if weight <= 1:
12 return 5
13 else:
14 if weight <= 5:
15 return 10
16 else:
17 return 20
18
19
20RATES = [(1, 5, 15), (5, 10, 25)]
21
22
23def shipping_after(weight, express):
24 for limit, standard, fast in RATES:
25 if weight <= limit:
26 return fast if express else standard
27 return 40 if express else 20
28
29
30cases = [(w, e) for w in (0, 1, 1.5, 5, 5.1, 50) for e in (False, True)]
31assert all(shipping_before(w, e) == shipping_after(w, e) for w, e in cases)
32print(len(cases), "cases identical")
33print(shipping_after(3, True), shipping_after(30, False))

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
The weight limits and prices now live in one table instead of being repeated in nested ifs.—
—
One expression replaces four branches of the old code.—
—
The characterization check compares both versions on all 12 inputs, including the boundary weights 1 and 5.—
—
Behavior is preserved on every tested case.—
12 cases identical
A 3 kg express parcel costs 25 and a 30 kg standard parcel costs 20, as before.—
12 cases identical
25 20

Behind the scenes

How Python processes this code

Python runs this source through its interpreter. It creates objects for values and resolves names as each statement runs.

Cost and common mistakes

The refactored function does O(r) comparisons for r rate rows, which is a constant for a fixed table. The old version used a fixed number of comparisons.

  • Refactoring without tests is guessing. Create tests that pin down current behavior before you change the structure.
  • Do not mix refactoring with new features or bug fixes in one change; it makes reviews and rollbacks hard.
  • Equal results on tested inputs do not prove equality on all inputs. Choose inputs at boundaries and in every branch.

Try it yourself

Add a third tier: weights up to 10 cost 15 standard and 30 express, and above that 20 and 40. Which line changes?

Reveal answer and reasoning

Add (10, 15, 30) to RATES after (5, 10, 25) and change the final fallback prices to 20 and 40. The loop needs no changes, which is the benefit of the table.

Further reading