Python · Chapter 3 · Concept lesson · 6 min read

What happens when you call a function

A function call gives the function its own temporary names, runs its body with them, and hands back one value with return. Anything else it touches is shared with the caller.

The idea

A function is a named piece of logic with a door on each side. Values go in through the parameters. One value comes out through return. In between, the function works in a private workspace of its own.

Figure 1

One call to line_total, from arguments to the returned value

  1. line_total(92.99, 1, 10) the call
  2. price=92.99, qty=1, discount_pct=10 arguments become parameters
  3. price * qty * (1 - discount_pct / 100) the body runs
  4. return round(..., 2) -> 83.69 one value goes back

Step 1 of 4 The line that calls the function gives it three values, in order. The call pauses while the function works.

  1. The line that calls the function gives it three values, in order. The call pauses while the function works.
  2. Python makes a fresh, private workspace and ties the parameter names to the arguments, matched by position (or by name if you write discount_pct=10).
  3. The body runs using only those names. 92.99 × 1 × 0.9 is 83.691. Nothing outside the function can see price or qty.
  4. return ends the function and sends one value back to where the call stood. The workspace is thrown away, and the call itself is replaced by 83.69.

When you call line_total(92.99, 1, 10), Python pauses your code, builds that workspace, ties the parameter names to the arguments you passed, runs the body, and replaces the whole call with the returned value. After that your code continues as if you had typed 83.69 where the call stood.

Why it exists

Copying a calculation and changing a number is how errors multiply. The discount rule gets pasted into nine places, someone fixes eight of them, and the totals in the ninth are wrong for a month. A function gives the rule one home with one name. Fix it there and every caller is fixed.

It also makes code readable. line_total(price, qty, discount_pct) says what is being computed. The same arithmetic inline says nothing until you decode it. In the pandas chapters you will pass functions to other functions, such as sorted(..., key=...) here and apply later, and that only works smoothly if you think of a function as a value you can name and hand over.

How it works under the hood

Parameters, arguments and defaults

A parameter is the name in the definition. An argument is the value in the call. They are matched by position, or by name when you write discount_pct=10. A default such as discount_pct=0 fills in when the caller leaves the argument out, which makes the common call short: line_total(11.99, 4).

Defaults are evaluated once, when the function is defined, so never use a list or dictionary as a default. Use None and create the list inside the body.

Return is the only way out

Figure 2

The same function, applied to three order lines

Order lines
priceqtydiscount_pct
92.99110
349.00110
11.9940
line_total(price, qty, discount_pct)
line_total
83.69
314.10
47.96

Step 1 of 3 Three real order lines. The rule for a line total is the same each time: price × quantity, less the discount.

  1. Three real order lines. The rule for a line total is the same each time: price × quantity, less the discount.
  2. Write the rule once as line_total(price, qty, discount_pct=0). The = 0 makes the discount optional: callers with no discount can leave it out.
  3. Call it once per line: 83.69, 314.10, and 47.96 for the line with no discount. One function, three calls, one rule to fix if the discount maths ever changes.

What a function gives back is whatever follows return. Two mistakes follow from this. A function with print and no return shows a value but gives back None, so the next line of code gets nothing to work with. And code after a return never runs, because the function has already finished. That second point is useful: early return lets you handle the special case first (if qty == 0: return 0) and write the main logic without extra indentation. A function can return several values by returning a tuple, return total, count, and the caller unpacks them: total, count = summarise(cart).

Local names stay local

A name assigned inside a function exists only during that call. A function can read a name from outside, but assigning to it creates a new local one. This is why two functions can both use a variable called total without interfering. It is also why you pass what a function needs in as arguments, instead of hoping the right global is lying around.

The exception: shared objects

Local names do not protect the objects they point to. If you pass a list in, the parameter is a second name for the same list. Change the list through it and the caller sees the change.

Figure 3

A function that edits the caller's list

Names
namepoints_to
priceslist A
plist A
list A
positionvalue
092.99
1349.00
list A after the sale
positionvalue
083.69
1314.10

Step 1 of 3 prices holds the list prices. apply_sale(prices) is called, and inside the function the parameter p is a second name for the same list, not a copy.

  1. prices holds the list prices. apply_sale(prices) is called, and inside the function the parameter p is a second name for the same list, not a copy.
  2. The body does p[0] = p[0] * 0.9 and so on. That edits list A itself, in place.
  3. Back in the caller, prices still points at list A, which now holds the sale prices. The list prices are gone. Build a new list inside the function and return it, and the original stays safe.

The safe pattern is the one pandas also follows: a function makes a new result and returns it, [p * 0.9 for p in prices], and leaves its input alone. If you do want a function to change its input, say so in its name and its docstring (apply_sale_in_place) so nobody is surprised.

Lambda and sorting

A lambda is a function with no name and one expression, written where it is needed: sorted(products, key=lambda p: p['price']). The key function is called once per item, and the items are ordered by what it returns. It is the same idea as line_total, just short enough to write inline. Anything longer than one expression deserves a proper def.

Functions that call functions

Once a function exists, another can use it. A weekly summary calls line_total for each line, adds the results and returns a dictionary with the total, the count and the biggest line. Each layer trusts the one below, so you test the small function first with a few inputs whose answers you can work out by hand: line_total(10, 3) should be 30, line_total(10, 3, 50) should be 15, and line_total(10, 0) should be 0. Those three checks cover the normal case, the discount and the empty line, and they take a minute to run. The edge cases you skip are the ones that reach production.

Common misconceptions

"print and return do the same thing"

print writes text to the screen for a person to read, and the program carries on without it. return hands a value to the code that called the function. In a notebook the two look alike, because the last value is displayed anyway. The moment you write x = f(...) and use x, only return works. If a variable ends up as None, suspect a missing return.

"Variables I change inside a function change outside too"

Assigning a name inside a function never touches a name outside. What can leak out is a change to a shared object, like a list modified in place. A good rule: reassigning is safe, append, sort and p[0] = ... on something you were handed are not. When in doubt, work on a copy with list(p) or build a new list.

"A function should do everything the task needs"

Small functions that do one thing are easier to test and reuse. A parser that cleans a price ("$1,249.00" into 1249.0) is one function. Totalling a supplier export is another that calls it. When the second breaks you know the first is fine, and when the format changes you edit a single function.

Check yourself

One question. Answering it ticks this lesson off on the map; it never locks a level.

A function is defined as def line_total(price, qty): print(price * qty). A learner writes total = line_total(10, 3) and then total + 5. What goes wrong?

Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.