Python 4.2Free video15 minLesson 2 of 4 in Module 4
Arguments and Scope
Pass arguments every way Python allows, and know where names live.
Free sample. Everything in this module opens with a free account.
Watch or read the lesson: Arguments and Scope
01A rate parameter
In the previous lesson, you wrote your own functions in Python. One of them was called with_tax. You gave it a price, and it calculated the final amount after adding eight percent tax. For example, when you passed in 12.50, it returned 13.5. That worked perfectly well.
But now imagine you are selling the same products in another city, and that city charges twenty percent tax instead of eight percent. What should you do?
You could create another function: one that adds eight percent tax, and another that adds twenty percent. But think about it. Both functions would do exactly the same calculation. The only difference would be the tax rate. So you would be repeating almost identical code, and that is exactly the kind of duplication functions are supposed to help you avoid.
A second parameter
Instead, you can make the existing function more flexible. Give it a second parameter called rate, and rather than hardcoding 0.08 inside the calculation, use that parameter.
There is one more detail. You can also give rate a default valueKey termThe value a parameter takes when a call does not pass that argument: rate=0.08.. That way, if someone does not provide a tax rate, the function automatically uses eight percent:
def with_tax(price, rate=0.08):
return round(price * (1 + rate), 2)
Three ways to call it
Now try calling this function in three different ways. First, provide just the price:
with_tax(12.50)
13.5
Python returns 13.5. Because you did not provide a rate, the function used the default value of 0.08.
This time, provide both the price and the rate:
with_tax(12.50, 0.20)
15.0
Python returns 15.0, because twenty percent of 12.50 is 2.50, so the total is fifteen dollars.
Finally, write the name of each parameter in front of its value:
with_tax(price=12.50, rate=0.20)
15.0
Again you get 15.0.
Two questions
All three calls use the same function, but they do not pass their arguments in exactly the same way. So how does Python know which value belongs to which parameter? And once a function starts running, which variables can it access? Can it change variables outside the function? Those are the two main questions this lesson explores.
02Matching arguments
Start with how Python matches arguments to parameters. Remember the difference from the previous lesson:
- A parameter is the name written in the function definition, like
priceandrate. - An argument is the value you provide when you call the function, like
12.50.
Positional arguments
Look again at the second call, with_tax(12.50, 0.20). It has two arguments, and neither one has a parameter name written in front of it. So Python matches them by their positions. The first argument, 12.50, goes to the first parameter, price. The second argument, 0.20, goes to the second parameter, rate.
These are called positional arguments, because their positions determine which parameters receive them.
Keyword arguments
Now look at the third call, with_tax(price=12.50, rate=0.20). Here, the parameter names are written explicitly. These are called keyword arguments, and Python matches them using those names.
That gives you something useful: with keyword arguments, you can change the order. You could write rate first and price second:
with_tax(rate=0.20, price=12.50)
15.0
You still get 15.0, because Python knows that 0.20 belongs to rate and 12.50 belongs to price.
Mixing the two
You can also mix positional and keyword arguments:
with_tax(12.50, rate=0.20)
15.0
That works too. The first argument is matched by position, and the second is matched by name. Just remember one rule: positional arguments come first, then keyword arguments.
Why use keyword arguments?
Compare these two calls: with_tax(12.50, 0.20) and with_tax(12.50, rate=0.20). Both produce the same result. But the second one tells you what 0.20 represents: it is the tax rate. When a function has several optional parameters, that extra clarity becomes really useful.
03Default values
Return to the first call you made, with_tax(12.50). You provided only one argument, but the function has two parameters. So why did Python not complain about a missing argument?
Because rate has a default value. Look at the definition again:
def with_tax(price, rate=0.08):
The equals sign in the parameter list tells Python that rate has a default value of 0.08. When you do not supply a rate, Python uses that default. But if you do supply one, the value you supply takes its place for that call.
So price is a required argument: every call must provide it. And rate is an optional argument: a call can leave it out.
04Collecting arguments
So far, your functions have accepted a fixed number of parameters. But what if you do not know how many arguments someone will pass?
Imagine you want a function that calculates the total of a customer's purchases. Sometimes they buy one item, sometimes three, sometimes ten. You do not want to define a separate function for every possible number of prices. So Python gives you a way to accept any number of positional arguments, using an asterisk.
*prices
Write a function called total, and inside its parameter list, write *prices:
def total(*prices):
print(prices)
return sum(prices)
What does that asterisk mean? It tells Python to collect all the extra positional arguments into one tuple. So when you call:
total(12.50, 8.25, 6.00)
(12.5, 8.25, 6.0)
26.75
Inside the function, prices is a tuple containing those three numbers. The function prints that tuple, then sum adds the numbers together, and the function returns 26.75.
You will often see this parameter written as *args, where args is short for arguments. But here is something important: the name itself is not special. You could call it *prices, *numbers, or something else. It is the asterisk that gives it this behavior.
Unpacking with an asterisk
The asterisk can also be used when calling a function. Suppose your prices are already stored in a list, and you want to pass those three values to total. If you write total(*prices), Python unpacks the list into separate positional arguments:
prices = [12.50, 8.25, 6.00]
total(*prices)
(12.5, 8.25, 6.0)
26.75
So the function receives three arguments, and returns 26.75.
Remember the difference:
- An asterisk in a parameter definition collects positional arguments.
- An asterisk in a function call unpacks an iterable into positional arguments.
**quantities
What if you want to accept any number of keyword arguments instead? For that, Python uses two asterisks. Write a function called order:
def order(**quantities):
return quantities
Now call it like this:
order(pasta=2, soup=1)
{'pasta': 2, 'soup': 1}
Python returns a dictionary. The key 'pasta' has the value 2, and the key 'soup' has the value 1. Why? Because **quantities collects the keyword arguments into a dictionary. The names you write in the call become the dictionary keys, and the corresponding arguments become the values.
*args and **kwargs
You will often see this written as **kwargs, where kwargs is short for keyword arguments. But again, the name kwargs is not special. The two asterisks are what matter.
*argscollects positional arguments into a tuple.**kwargscollects keyword arguments into a dictionary.
05Keyword-only arguments
There is another use of the asterisk that you need to recognize, and this one is a little different. Suppose you redefine the tax function like this:
def with_tax(price, *, rate=0.08):
return round(price * (1 + rate), 2)
Notice the asterisk sitting on its own, between price and rate. That asterisk does not collect arguments. Instead, it marks the beginning of keyword-only parameters. Every parameter after it must be supplied by keyword if you want to override its default.
A rate by position
Try passing the rate by position, as you did before:
with_tax(12.50, 0.20)
TypeError: with_tax() takes 1 positional argument but 2 were given
Now Python raises a TypeError, because the function accepts only one positional argument. The rate can no longer be supplied as the second positional argument.
A rate by keyword
The correct call uses rate as a keyword argument:
with_tax(12.50, rate=0.20)
15.0
It works, and returns 15.0. So making a parameter keyword-only can help prevent confusion, especially when a function has several optional settings.
06Local names and scope
Now for the second big question. When a function is running, which variables can it access? And what happens when it tries to change them? This brings you to something called scopeKey termThe part of a program where a name can be used. Python looks up names in local, enclosing function, global and built-in scopes; reading an unbound local raises UnboundLocalError..
A name created inside a function
Start with an example. Write a function called taxed_total that takes a collection of prices. Inside the function, it creates a new list called taxed, which contains the prices after adding eight percent tax. Then it returns their sum:
def taxed_total(prices):
taxed = [round(p * 1.08, 2) for p in prices]
return sum(taxed)
Call it with the three menu prices:
taxed_total([12.50, 8.25, 6.00])
28.89
So the function works. But here is a question. What happens if you try to access taxed outside the function, after the call, with print(taxed)? Think it through before reading on.
I have a guess. Show meHide the answer
print(taxed)
NameError: name 'taxed' is not defined
Python raises a NameError, because taxed is a local variableKey termA name in a function's local scope; enclosing bindings can survive through a returned closure.. It was created inside the function, and its name belongs to that function's local scope.
Scope
Remember execution frames from the previous lesson? Every time you call a function, Python creates a frame for that call, and the function's local variables belong to that frame. So while taxed_total is running, the name taxed exists in that function's local namespace. But when the function finishes, that local name is not available to the code outside it.
That is the idea of scope. ScopeKey termThe part of a program where a name can be used. Python looks up names in local, enclosing function, global and built-in scopes; reading an unbound local raises UnboundLocalError. determines where a name can be accessed in a program.
The same spelling in two places
Parameters are local names too. Here is an example. Create a variable outside the function, call it price, and set it to 99. (This cell also restores the original with_tax, with an ordinary rate parameter.)
def with_tax(price, rate=0.08):
return round(price * (1 + rate), 2)
price = 99
Now call the function. What happens to price outside it? Think it through before reading on.
I have a guess. Show meHide the answer
with_tax(12.50)
13.5
Inside the function, price refers to 12.50, because that is the argument you passed, and the function returns 13.5. But after the call, if you print your original price variable, it is still 99:
print(price)
99
The parameter price inside the function is a local name. The price variable outside the function is a different name, in a different namespace. They happen to have the same spelling, but they belong to different scopes. So changing which object the local parameter refers to does not change the global variableKey termA name bound at the top level, which any function can read. Rebinding it inside a function needs global., the one defined at the top level of the program.
07Name lookup and globals
A function can also read variables that were not defined inside it. Say you have a variable called rate at the top level of your program, set to 0.20. Then you define a function, city_price:
rate = 0.20
def city_price(price):
return round(price * (1 + rate), 2)
Notice that rate is not a parameter, and it is not assigned inside the function. But when you call it:
city_price(12.50)
15.0
You still get 15.0. How did the function find rate?
The LEGB rule
When Python evaluates a name, it follows a lookup order:
- It first checks the function's local scope.
- If the name is not there, it checks any enclosing function scopes.
- Then it checks the global scope.
- Finally, it checks the built-in scope.
This is commonly called the LEGB rule: Local, Enclosing, Global, Built-in.
In this example, city_price has no local variable called rate, and there is no enclosing function scope containing rate. So Python looks in the global namespace, finds rate equal to 0.20, and uses that value.
You might be wondering what an enclosing scope is. That is relevant when you define a function inside another function: the inner function can access names from the outer function's scope. You will explore that more deeply later in the course.
Changing a global from inside a function
Here is a question that catches a lot of people. Create a global variable called count and set it to zero. Then define a function called record, whose body is count += 1 and nothing else:
count = 0
def record():
count += 1
What happens when you call record? Will Python find the global count and increase it to one, or will something else happen? Think it through before reading on.
I have a guess. Show meHide the answer
record()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
Python raises an UnboundLocalError. Why? Remember that += is an assignment. When Python sees an assignment to a name inside a function, it treats that name as local to the function, unless you explicitly tell it otherwise. So even though there is a global variable called count, Python treats count inside record as a local variable.
Here is the problem. count += 1 needs to read the current value of count before adding one. But that local variable has not been assigned a value yet. So Python raises UnboundLocalError.
The global statement
How do you fix it? One option is the global statement:
count = 0
def record():
global count
count += 1
The global statement tells Python that count inside the function refers to the global name. So when you call record, it updates that global variable. Call it once, and count becomes 1:
record()
print(count)
1
Call it again, and count becomes 2:
record()
print(count)
2
That works. But there is a reason you do not always want to write functions this way. The function is changing a variable outside itself, and that change is not obvious just from looking at the call record(). So when possible, it is often clearer to pass the value into the function and return the updated result:
def record(count):
return count + 1
visits = 0
visits = record(visits)
visits = record(visits)
visits
2
Here every change is visible at the call: visits = record(visits) says exactly which variable is being updated.
Built-in names
Now the last part of the LEGB rule: built-in. That is where names like print, len and sum are found.
But be careful here. Python checks local and global names before built-in names. So what happens if you create your own variable called sum?
sum = 0
Now suppose you write sum([1, 2]). What do you think happens? Think it through before reading on.
I have a guess. Show meHide the answer
sum([1, 2])
TypeError: 'int' object is not callable
Python raises a TypeError, because it finds your variable sum first, and that variable refers to the integer zero. So Python tries to call the integer as if it were a function, and integers are not callable.
So be careful when naming variables. Avoid names like sum, list, max or input for your own variables. Those names already belong to useful built-in objects, and reusing them can cause confusing errors.
08Recap and challenge
You started with the with_tax function and made it more flexible by adding a second parameter for the tax rate. Then you learned how Python matches arguments to parameters:
- Positional arguments are matched by their positions, and keyword arguments are matched by their names. Positional arguments come first.
- Default values make parameters optional.
- A single asterisk in a parameter list, as in
*args, collects positional arguments into a tuple. - Two asterisks, as in
**kwargs, collect keyword arguments into a dictionary. - A bare asterisk makes the parameters after it keyword-only.
Then you moved to scope, and learned how Python looks up names using the LEGB rule: Local, Enclosing, Global and Built-in. You saw why assigning to a name inside a function normally makes it local, and how the global statement can change that behavior.
Your challenge
Write a function called receipt:
- It should accept any number of prices.
- It should also accept a tax rate, with a default of eight percent. The rate must be a keyword-only argument.
- It should calculate the total including tax.
- It should return the result as a string, with a dollar sign and exactly two decimal places.
You will need to combine collecting positional arguments with a default keyword-only parameter. And remember, the function should return the formatted string, not just print it. Here is a starting point:
def receipt(*prices, rate=0.08):
# Write your solution here
pass
Test it with receipt(12.50, 8.25, 6.00), and once it works, with a different tax rate too, such as receipt(12.50, 8.25, rate=0.20). If you are working in the same notebook as this lesson, sum still refers to 0, so run del sum first to get the built-in back. Try solving it yourself before looking back at the examples.
So far, your functions have called other functions, like round, sum and print. But what if a function calls itself? That is the idea behind recursion, and it is what the next lesson explores.
Key terms: Arguments and Scope
The words this lesson introduces, each in one line. The module’s glossary collects them all, and the AI glossary has every term across the courses.
*args- A parameter with one star, which collects extra positional arguments into a tuple:
def total(*prices). - default value
- The value a parameter takes when a call does not pass that argument:
rate=0.08. - global variable
- A name bound at the top level, which any function can read. Rebinding it inside a function needs
global. - keyword argument
- An argument passed as
name=value, matched to the parameter with that name:rate=0.2. - keyword-only parameter
- A parameter after
*or*args, which must be passed by keyword; it can be required even after a parameter with a default. **kwargs- A parameter with two stars, which collects extra keyword arguments into a dictionary:
def order(**quantities). - local variable
- A name in a function's local scope; enclosing bindings can survive through a returned closure.
- nonlocal
- A declaration that assignments use a binding in an enclosing function scope, rather than creating a local binding.
- positional argument
- An argument matched to a parameter by its position.
- scope
- The part of a program where a name can be used. Python looks up names in local, enclosing function, global and built-in scopes; reading an unbound local raises
UnboundLocalError. - signature
- A function's name and its parameters, as the documentation lists them:
sorted(iterable, /, *, key=None, reverse=False).
Quiz: Arguments and Scope
Your first pick on each question is the one that counts. Getting one wrong here is how the lesson sticks.