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
-
line_total(92.99, 1, 10)the call -
price=92.99, qty=1, discount_pct=10arguments become parameters -
price * qty * (1 - discount_pct / 100)the body runs -
return round(..., 2) -> 83.69one value goes back
- The line that calls the function gives it three values, in order. The call pauses while the function works.
- 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). - The body runs using only those names. 92.99 × 1 × 0.9 is 83.691. Nothing outside the function can see
priceorqty. returnends 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
| price | qty | discount_pct |
|---|---|---|
| 92.99 | 1 | 10 |
| 349.00 | 1 | 10 |
| 11.99 | 4 | 0 |
| line_total |
|---|
| 83.69 |
| 314.10 |
| 47.96 |
- Three real order lines. The rule for a line total is the same each time: price × quantity, less the discount.
- Write the rule once as
line_total(price, qty, discount_pct=0). The= 0makes the discount optional: callers with no discount can leave it out. - 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.
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.
Not quite. print shows the 30 on screen. It does not hand it back, so total does not receive it.Try another answer.
Right. A function without return gives back None. The 30 went to the screen, and total + 5 then fails with a TypeError.
Not quite. total is a name in the caller, assigned the returned value. It is not local to the function. The value is missing because nothing was returned.Try another answer.
Not quite. print works anywhere. The call runs and shows 30; what breaks is the line that tries to use the result.Try another answer.
Lesson done: it is ticked on the map.