top of page

Extract Function — Refactoring Patterns, Part 2

Shift Quality Editorial
Jul 24
3 min read

Updated: Oct 4

Refactoring Patterns · Part 2

Extract Function is the most-used refactoring there is, and for good reason: a long function is really several ideas crammed together, and pulling each into its own well-named function turns a wall of code into a readable outline. This walks through doing it safely — spotting the block worth extracting, naming it for intent rather than mechanics, and moving it without changing behavior.


Extract function is the bread-and-butter refactoring. When a block does something nameable, give it a name.


Step 1: Spot the Opportunity (10 min)


Signs to extract:


  • Comment above a block of code explaining what it does

  • A repeated block

  • Block of 5+ lines doing one logical thing

  • A complex expression nested in a larger function


The comment is often the function name.


Step 2: Extract a Named Block (10 min)


# Before
def render_invoice(order):
    print(f"Invoice for {order.customer.name}")
    print(f"Date: {order.date}")
    print(f"---")
    # calculate totals
    subtotal = sum(item.price for item in order.items)
    tax = subtotal * 0.08
    total = subtotal + tax
    print(f"Subtotal: {subtotal}")
    print(f"Tax: {tax}")
    print(f"Total: {total}")

The comment "calculate totals" signals the extraction.


# After
def render_invoice(order):
    print(f"Invoice for {order.customer.name}")
    print(f"Date: {order.date}")
    print(f"---")
    subtotal, tax, total = calculate_totals(order)
    print(f"Subtotal: {subtotal}")
    print(f"Tax: {tax}")
    print(f"Total: {total}")

def calculate_totals(order):
    subtotal = sum(item.price for item in order.items)
    tax = subtotal * 0.08
    total = subtotal + tax
    return subtotal, tax, total

Function name replaces comment. Reads as story.


Step 3: Name Captures Intent (10 min)


# Bad name
def do_stuff(x):
    ...

# Good name
def calculate_shipping_cost(order):
    ...

Name tells you what without reading the body.


If you can't name it: maybe the extraction isn't right (no single concept inside).


Step 4: Identify Inputs and Outputs (10 min)


For the block you want to extract:


  • What variables does it read? → parameters

  • What variables does it write that are used later? → return values

  • What does it mutate? → consider returning new value instead


If too many params: maybe object that bundles related fields.


Step 5: Small Functions Win (10 min)


"Functions should do one thing."

Heuristic: 5-15 lines is comfortable. Longer = candidate for further extraction.


Not religion: some functions are naturally longer.


But suspect long functions. They usually have multiple concerns.


Step 6: Extract Without Behavior Change (15 min)


Process:


  1. Add the new function with the extracted code

  2. Replace original block with call

  3. Run tests

  4. Refine names


Each step small. Tests after each.


# Step 1: add
def calculate_totals(order):
    subtotal = sum(...)
    ...

# Step 2: replace
def render_invoice(order):
    ...
    subtotal, tax, total = calculate_totals(order)
    ...

Don't change names + behavior at once. Confusion + bugs.


Step 7: Use IDE Refactoring (5 min)


Most IDEs:


  • VS Code: select block → "Extract function"

  • PyCharm / IntelliJ: select → Refactor → Extract method

  • Visual Studio: same


IDE handles parameter passing, return values, scoping.


Use it. Manual extraction = typos.


Step 8: Beware Premature Extraction (10 min)


def is_positive(n):
    return n > 0

Extracting n > 0 into is_positive(n): usually NOT helpful. The original is just as clear.


Extract when:


  • Block is complex enough that a name adds clarity

  • It's reused

  • It's testable independently


Don't extract trivial expressions.


Step 9: After Extract, Look at the Caller (10 min)


Often, after extraction, the caller becomes much clearer:


def render_invoice(order):
    print(invoice_header(order))
    print(invoice_lines(order))
    print(invoice_totals(order))
    print(invoice_footer(order))

Now reads as table of contents. Each helper does its thing.


This is the win. Many small functions, top-level reads like a story.


Step 10: Refactor Tests in Parallel (10 min)


# Now you can unit-test the helper
def test_calculate_totals():
    order = make_order(items=[Item(10), Item(20)])
    subtotal, tax, total = calculate_totals(order)
    assert subtotal == 30
    assert tax == 2.4
    assert total == 32.4

Tests are easier with small functions. Less mocking.


What You Just Did


Extract function: spot opportunities, name well, IO, small, no-behavior-change, IDE, when not to, caller clarity, parallel tests.


Common Failure Modes


Bad names. do_calculation, handle_thing.


Too many parameters. Extract a struct.


Behavior change snuck in. Tests catch — if they exist.


Extracted at wrong scope. Module function when method belongs in class.


Over-extracted. 100 two-line functions; harder to follow.



Continue the Refactoring Patterns path



Part of the Refactoring Patterns learning path.

bottom of page