Function Arguments, Return Values, and Scope
Pass values into a function, return a result, and distinguish local names from names in the caller.
Video lesson: Function Arguments, Return Values, and Scope
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
This function calculates the right number inside its body, but the next line still crashes. The price is twenty and the tax rate in this first example is twenty percent, so we expect a total of twenty-four. Why can Python calculate that value and then say it cannot find total? The answer is in the boundary between a function and its caller. In this lesson we will follow an argument into a parameter, watch a local variable being created, and return a value back to the caller. We will also use a default argument so a common rate can be omitted and a special rate can be supplied explicitly. The browser lesson includes worked examples, a checked coding task, and a quiz.
- 00:43
Here I want you to watch each real terminal run and ask what value the call itself produces. A visible printout and a returned value are different things. A parameter is the name in the function definition; an argument is the value you supply at the call site. Following one argument into a parameter is a useful way to understand where the calculation gets its input. The traceback ends with NameError because total has not been defined in the caller's scope. It was assigned inside total_with_tax, where it is a local name. The call passes twenty to the parameter price, then the function evaluates twenty times one point two and stores twenty-four in its local total.
- 01:26
After the function body ends, the call has no explicit return statement, so its value is None. The later print does not look inside the finished call; it searches for a name called total where that print statement is running. There is no such name there. This error is useful evidence because it tells us which name Python could not resolve, and the highlighted line tells us where the lookup happened. We should not try to repair it by adding a global variable. The function already has the value. It needs to hand that value back to its caller. The calculation inside the function did happen before the error. The crash is on the later print of total, after the call has finished.
- 02:09
That timing tells us to inspect what value crossed back from the function to the caller. Let us name the pieces precisely. The twenty in total_with_tax called with twenty is an argument: a value supplied by the caller. The word price in the definition is a parameter: a local name that receives that argument for this call. The assignment to total also creates a local name. None of these names automatically becomes a variable in the caller. Return is the route out. A function call is an expression, and a return statement decides the value of that expression. If we write print inside the function instead, the user may see twenty-four on the screen while the call still evaluates to None.
- 02:54
Save that call in a variable and you would save None, not the printed number. Calculation functions are easier to reuse when they return data and let the outer program decide whether to print, format, or store it. The repair is one extra statement at the end of the function body. Without an explicit return, a Python function returns None. You can print the call itself to confirm that observation, then remove that temporary diagnostic. It separates the computed local value from the value the caller actually receives. Now the local total is returned. The call expression evaluates to twenty-four point zero, and the outer print displays that returned number.
- 03:38
We never need to reference the local name from outside. This design also lets us call the same function with a different price. Each call receives its own local price and total. The second call does not overwrite a variable inside a previous call that the caller is meant to reach. If the caller wants to keep a result, it can assign result equals total_with_tax called with twenty. That result is in the caller's scope, so it can be printed or used in another calculation later. The lesson's practical task begins with the calculation already stored in total. You add a return of round total to two decimal places.
- 04:18
Do not put a print in place of return: the task checker calls your function with several values and needs the actual numeric answer. A return statement ends that execution path and hands a value back to the call expression. Printing inside the function would display the total, but it would not make the total available for another calculation in the caller. A default parameter gives the caller a shorter form for a common case. In the first call we pass only the price, so rate takes its default value of zero point two and the result is twenty-four point zero. In the second call we pass zero as the rate, overriding the default for that call, so the result is twenty.
- 05:01
In ordinary code I would often write rate equals zero at the call site to make the second argument's meaning obvious. Defaults should be chosen deliberately; real tax rates depend on context, so this is a focused function exercise, not a complete tax model. Before you finish, predict a call with a price of nineteen point ninety-nine and a ten percent rate. The rounded result should be twenty-one point ninety-nine. Try it in the browser console and press Check task. The checker also tests zero rate and a different price, which catches a solution that only prints the sample answer. Complete the quiz, then explain to yourself why print(total) failed even though the function had computed a total.
- 05:46
If you can trace that boundary, many function bugs become much easier to diagnose. Try passing a price of zero and then a rate of zero. Ask what each call should return before running it. Defaults make common calls shorter, while an explicit override documents an unusual rate at the call site.
Understand the concept
A parameter is the name in a function definition; an argument is the value a caller supplies. In def total_with_tax(price), price is the parameter. In total_with_tax(20), 20 is the argument for that call. Each call receives its own local price, so the same function can calculate for several amounts without rewriting the formula.
A return statement passes a value back to the call site. Printing from inside a function displays text, but it does not hand the value to the caller. If a function finishes without return, the call evaluates to None. Use return when another part of the program needs the result; let the caller decide how to display it.
A name assigned inside a function is local by default. If total is assigned inside total_with_tax, a later print(total) outside that function raises NameError unless some separate outer variable named total exists. Return the value and store or print the function call instead. A local name can be reused safely by different calls.
A default parameter gives a value when the caller omits that argument. For example, rate=0.20 can mean 20 percent unless the caller passes a different rate. Put required parameters before defaulted ones. Keep the meaning of each argument clear; a keyword argument such as rate=0 makes a special case easy to read.
- Parameters receive arguments for each call
- return gives a value back to the caller
- Names assigned inside a function are local
- Default parameters can be overridden
See it step by step
Read the code, predict the output, then compare it with the result.
01. Pass a positional argument
def square(value):
return value * value
print(square(4))
print(square(7))16
49The parameter value receives 4 for the first call and 7 for the second. Each return is printed by the caller.
02. Override a default by keyword
def total_with_tax(price, rate=0.20):
return round(price * (1 + rate), 2)
print(total_with_tax(20))
print(total_with_tax(20, rate=0))24.0
20The first call uses the default 20 percent. The named rate=0 argument replaces that default in the second call.
03. Return a local value
def make_label(name):
label = f"Hello, {name}"
return label
print(make_label("Ada"))Hello, Adalabel exists inside the function. Returning it gives the caller a value it can print or save.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Track each call from argument to return
When a function is defined, its body is not yet run. A call supplies arguments, Python binds them to parameter names for that call, and then the body executes. Trace this with two calls using different prices. The local name total is created separately each time; the second call does not continue where the first one left off. A return statement sends the calculated value back to the particular call expression, which can then be printed, assigned, or used in another expression.
A name assigned inside the function is normally local. That means a print(total) outside the function cannot read the local value after the call ends. Catching NameError in this demonstration keeps the guide runnable, but production code should solve the actual problem by returning the value. Predict the two totals before running, then point to the line where the name leaves the function and becomes accessible to the caller as a returned result.
def total_with_tax(price):
total = price * 1.2
return total
print(total_with_tax(20))
print(total_with_tax(10))
try:
print(total)
except NameError as error:
print(type(error).__name__)24.0
12.0
NameError- The first call binds price to 20 and returns 24.0.
- The second call binds its own price to 10 and returns 12.0.
- The caller can use the returned values, but cannot read the function's local total name.
Display is different from delivering a value
The first double function below prints eight from inside the body. That proves the calculation ran, but the call itself still evaluates to None because no value was returned. The caller saves that None in printed_result. This becomes a problem as soon as the program tries to add to the result, compare it, or pass it to another function. A print statement is intended for a human viewing output; return is how code communicates a result to its caller.
The second function returns eight instead. Its caller can save and print that number. The two functions can produce similar visible lines, so inspect the variable held by the caller rather than trusting the console appearance alone. A practical design is to keep calculation functions free of display and handle formatting or messages at the outer edge of the program. If you later want to reuse the function in a web app or a test, a returned value remains useful.
def double_and_print(value):
print(value * 2)
printed_result = double_and_print(4)
print(printed_result)
def double_and_return(value):
return value * 2
returned_result = double_and_return(4)
print(returned_result)8
None
8- The first 8 comes from inside double_and_print, not from its return value.
- A function without return gives its caller None.
- The second function hands 8 to the caller, which can save or print it.
Defaults make the usual case short
A default parameter is used only when a call omits that argument. In this example, rate has a default of 0.20, so total_with_tax(20) applies twenty percent. A call with rate=0 deliberately overrides the default and returns the original price. The keyword spelling makes the unusual case easy to recognize. You could also pass the rate positionally, but readers then need to remember what the second number means.
Defaults should represent a sensible convention for your function, not a guess about every business rule. Tax rates vary by place and product; this function is a focused example rather than a general tax calculator. The round call produces a practical two-decimal result for the lesson, although strict accounting systems need explicit monetary rules. Try a price that produces more than two decimal places and compare the unrounded intermediate with the returned number.
def total_with_tax(price, rate=0.20):
return round(price * (1 + rate), 2)
print(total_with_tax(20))
print(total_with_tax(20, rate=0))
print(total_with_tax(19.99, rate=0.10))24.0
20
21.99- The omitted rate uses the default 0.20.
- A keyword argument replaces that default for one call.
- Change rate to 0.05 and predict the rounded third result.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
The calculation is already stored in the local variable total. Return round(total, 2). The caller's print(total_with_tax(20)) displays the returned value.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Trying to read a local variable outside its function
def total_with_tax(price):
total = price * 1.2
total_with_tax(20)
print(total)def total_with_tax(price):
total = price * 1.2
return total
print(total_with_tax(20))What happens: NameError: name 'total' is not defined.
Return the local value. The call expression then provides it to the caller.
Printing instead of returning
def double(n):
print(n * 2)
result = double(4)
print(result)def double(n):
return n * 2
result = double(4)
print(result)What happens: The output contains 8 and then None.
Printing is a side effect. A return statement makes the value available for assignment and further calculation.