Chaining Functions Together

Introduction: Small Functions That Cooperate

Welcome back to Returning Values and Understanding Scope in Python! We are now in the third unit of this course. In Unit 1, we saw that return sends a single value out of a function; in Unit 2, we saw that for the pure calculation functions in this course, parameters move values in and return explicitly provides each function's result. The natural next question is what happens when we take that returned value and hand it straight to another function.

That is what this lesson is about. Our example is an order calculation: a quantity and a unit price go in, and a formatted receipt line comes out, built by three tiny functions that each do exactly one job. The whole program produces a single line:

Total due: $36.00

Four ideas carry us there:

  1. a returned value is an ordinary value, so it can be used as an argument;
  2. stages can be connected by nesting calls or by naming each result;
  3. each stage's return type determines where it can sit in the chain;
  4. a missing return breaks the entire chain, not just one function.

Three Single-Purpose Functions

Let's meet the three functions that do the work. Each one is a single line of logic, and each one has a clear contract: something goes in, and something comes back.

def subtotal(quantity, price):
    return quantity * price


def with_tax(amount):
    return amount * 1.2


def to_label(amount):
    return f"Total due: ${amount:.2f}"

Reading them by what they take in and hand back:

  • subtotal takes two numbers and hands back a number: the raw order total;
  • with_tax takes one number and hands back a number: the same total plus 20% tax;
  • to_label takes one number and hands back a string: the finished receipt text.

Notice that none of them prints, following the value-producing style from Unit 1. Inside to_label, an f-string formats the amount with exactly two digits after the decimal (:.2f), so 36.0 displays as $36.00. We use ordinary floats here only to keep the return-value example simple; production finance code should usually track money as integer cents or with decimal.Decimal instead. As always, all three definitions sit above any line that calls them.

Naming Each Stage: The Pipeline in Three Lines

With the three helpers in place, the main flow reads almost like a sentence. Here it is exactly as it appears in the finished program:

# The output of one function feeds directly into the next
base = subtotal(3, 10)
final = with_tax(base)
print(to_label(final))

Tracing it line by line: subtotal(3, 10) computes 3 * 10 and returns 30, so base holds 30. Next, base is passed in as with_tax's amount parameter; 30 * 1.2 gives 36.0, so final holds 36.0. Finally, to_label(36.0) builds "Total due: $36.00", and only that returned string ever reaches print.

The crucial insight is that with_tax(base) involves no special syntax at all: base is simply a variable holding a number, and Python neither knows nor cares that a function produced it.

Pipeline showing returned values moving through subtotal, with_tax, and to_label to the printed receipt line

One detail is worth explaining: multiplying by the float 1.2 produces the float 36.0, and :.2f formats that value with two decimal places for display as $36.00. Floats are fine for this teaching example; they are not a model for production money math.

The Same Pipeline as One Nested Expression

The three named lines are not the only way to connect the stages. Because a returned value can be used as an argument directly, we can write the whole chain as one expression:

print(to_label(with_tax(subtotal(3, 10))))

This is read inside-out. The innermost call runs first: subtotal(3, 10) returns 30, and that number replaces the call, so the expression effectively becomes to_label(with_tax(30)). Then with_tax(30) returns 36.0, leaving to_label(36.0), which returns "Total due: $36.00". Only then does print receive anything. The same three intermediate values appear in both styles.

So which should we prefer? They evaluate identically, but the named version makes every intermediate value inspectable and lets the reading order match the execution order, while the nested version is compact yet harder to debug. Our final program uses the named-stage form for exactly that reason.

Returned Values Are Just Values

Being usable as an argument is only part of the story. A returned value behaves like any other value, so it can also sit inside a larger expression. Here is a short aside that is not part of the final program:

combined = subtotal(3, 10) + subtotal(2, 4)   # 30 + 8 -> 38
print(to_label(with_tax(combined)))           # Total due: $45.60

The evaluation order on the first line is worth spelling out: both calls run, each returned number replaces its own call, the addition 30 + 8 happens, and only then does combined receive 38. The second line pushes that number through the remaining stages, and 38 * 1.2 becomes 45.6, which to_label formats as $45.60.

The general rule is simple: a returned value can appear anywhere a value is allowed, whether as an argument, as an operand in arithmetic, or as the right-hand side of an assignment.

Growing and Reordering a Pipeline

When a Stage Forgets to Return

Chains are only as strong as their links, and the weakest link is a function that computes something but never hands it back. Recall from Unit 1 that a function without a return gives back None:

def with_tax(amount):
    amount * 1.2        # computed, but never handed back


base = subtotal(3, 10)
final = with_tax(base)  # final is None
print(to_label(final))  # TypeError inside to_label

The multiplication really does happen; the result is simply thrown away when the call ends, exactly as local scope predicts. So final holds None, and the crash appears inside to_label, which tries to format None with :.2f.

That is the part worth remembering: the error surfaces in the next stage, not in the buggy one. A broken link poisons everything downstream, so in a pipeline every stage must return — otherwise the next stage receives None instead of a usable value.

The Complete Program

Putting every piece together, here is the finished script in its exact final shape:

def subtotal(quantity, price):
    return quantity * price


def with_tax(amount):
    return amount * 1.2


def to_label(amount):
    return f"Total due: ${amount:.2f}"


# The output of one function feeds directly into the next
base = subtotal(3, 10)
final = with_tax(base)
print(to_label(final))

Every choice is deliberate: no imports, no main() wrapper, no __main__ guard, every definition above its first call, no print inside any function body, and exactly one top-level print. The payoff is flexibility; changing the first capture to base = subtotal(5, 10) prints Total due: $60.00 with no edits to with_tax or to_label.

Total due: $36.00

Conclusion and Next Steps

In one sentence: because a returned value is an ordinary value, small single-purpose functions can be chained into a pipeline where each stage's output becomes the next stage's input. Around that idea sit three supporting rules: nesting and named stages evaluate the same way but read very differently; a stage's position is set by the types it takes in and hands back; and every stage must return, or the chain quietly breaks downstream.

In the practices ahead, we will trace a value through three functions, chain all the calls in one nested expression, insert a brand-new stage without touching a single existing function, combine two return values inside a larger expression, and finally rewrite the flow with each stage named. After that, the last unit of this course adds docstrings, so each function's contract — what it takes in and what it hands back — is written down for anyone reading the code.

Let's open the editor and wire these little functions together!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal