Learn
04.2 | PARAMETERS AND VARIABLE SCOPE
01 | PARAMETERS ARE LOCAL INPUT NAMES
Consider a function calculating ticket cost. It needs a quantity and a unit price. The definition lists two parameters; each call supplies arguments to bind to those local names. Local means the names belong to that particular call rather than being created as top-level variables.
Different callers can use different names for their data. tickets = 3 and price = 4.5 can be passed into parameters quantity and unit_price. The values matter; matching spelling is unnecessary.
The parameter names help describe the contract. Choose quantity rather than a and unit_price rather than b so a reader can understand the operation without tracing every statement.
def ticket_cost(quantity, unit_price):
return quantity * unit_price
tickets = 3
price = 4.5
print(ticket_cost(tickets, price))EXPECTED OUTPUT
13.5
02 | POSITIONAL AND KEYWORD ARGUMENTS
Positional arguments match parameters by their order. ticket_cost(3, 4.5) binds quantity to 3 and unit_price to 4.5. Swapping arguments can create a logic error even when the arithmetic happens to give the same product.
Keyword arguments name the parameter they target: ticket_cost(unit_price=4.5, quantity=3). They improve clarity when several arguments have similar types. A call can combine positional and keyword arguments, with ordinary positional arguments before keyword ones.
Do not supply the same parameter twice, omit a required argument or invent a keyword the function does not accept. Python reports TypeError rather than guessing your intent.
def format_score(name, score):
return f"{name}: {score}"
print(format_score("Maya", 8))
print(format_score(score=8, name="Maya"))EXPECTED OUTPUT
Maya: 8 Maya: 8
03 | DEFAULTS PROVIDE AN OPTIONAL INPUT
A default parameter value is used when the caller omits that argument. def greet(name, greeting="Hello") allows greet("Maya") and greet("Maya", "Welcome"). In an ordinary signature, required parameters come before parameters with defaults.
Defaults are evaluated when the definition executes, not afresh for every call. Immutable defaults such as strings or numbers are usually straightforward. A list used as a default can be shared across calls, retaining changes unexpectedly.
Use None as a sentinel for an omitted mutable collection, then create a new list inside the body. Test whether it is None, rather than using if not items, because a caller may deliberately supply an existing empty list.
def add_note(note, notes=None):
if notes is None:
notes = []
notes.append(note)
return notes
print(add_note("first"))
print(add_note("second"))EXPECTED OUTPUT
['first'] ['second']
04 | LOCAL VARIABLES BELONG TO A CALL
Scope describes where a name can be accessed. A variable assigned inside a function is normally local to that function. When the caller tries to use that local name directly, it is not automatically available. Return the information the caller needs.
Each call has separate local bindings. A second call to the same function does not reuse the earlier call’s local total merely because its spelling is the same. The returned object may live on when the caller keeps a reference to it.
This separation makes functions easier to reason about. You can use a descriptive name total in several independent functions without forcing them to share one top-level value.
def doubled(number):
total = number * 2
return total
result = doubled(6)
print(result)
# print(total) here would raise NameError.EXPECTED OUTPUT
12
05 | THE SAME SPELLING CAN MEAN DIFFERENT BINDINGS
A global name is defined at module level. A function can read such a name when it has no local binding of that name. But assigning a name inside the function normally makes that name local for the whole function body.
If the module has score = 10 and a function assigns score = 3, the local score shadows the global one. Shadowing means the local binding is found in that scope instead of the outer binding. The global score is not replaced by this ordinary local assignment.
Python looks through local, enclosing-function, global and built-in scopes when resolving an ordinary name. This course starts with local and module-level names. Do not assume that names in a separate caller’s local scope are automatically visible to a called function.
score = 10
def show_local_score():
score = 3
print("Inside:", score)
show_local_score()
print("Outside:", score)EXPECTED OUTPUT
Inside: 3 Outside: 10
06 | WHY A GLOBAL UPDATE CAN FAIL
If a function contains score = score + 1 without declaring global score, Python treats score as a local name. Reading that local name before it has been assigned raises UnboundLocalError. It does not first read the module score and then quietly create a new local one.
global score explicitly makes assignments in that function target the module-level binding. It can be appropriate in some designs, but hidden shared state makes reuse and testing harder. Prefer accepting the old value and returning the new value when possible.
The caller then owns the update: score = add_point(score). This clearly shows where data enters and where the result is stored.
def add_point(current_score):
return current_score + 1
score = 10
score = add_point(score)
print(score)EXPECTED OUTPUT
11
07 | REBINDING IS DIFFERENT FROM MUTATION
An argument supplies an object reference that is bound to a local parameter. Python does not automatically make a fresh copy of every argument. A list passed to a function can therefore be the same list seen by the caller.
Assigning items = [99] rebinds the local parameter to a new list. Calling items.append(99) changes the original shared list object. These are different operations, even though both mention items.
This behaviour is sometimes summarised as object sharing. It is more precise than saying every argument is copied or that a parameter always acts as an alias for the caller’s variable itself. The local name can be rebound independently.
def replace_locally(items):
items = [99]
def append_to_shared(items):
items.append(99)
scores = [8, 5]
replace_locally(scores)
print("After rebind:", scores)
append_to_shared(scores)
print("After append:", scores)EXPECTED OUTPUT
After rebind: [8, 5] After append: [8, 5, 99]
08 | MAKE EFFECTS CLEAR IN THE CONTRACT
A function can deliberately change a supplied list, but its caller should know. A name such as append_score and a docstring saying it mutates the list make the effect clearer. An alternative returns a new list, leaving the original unchanged.
A copy of a flat list of numbers is enough for this example. A shallow copy of nested lists still shares inner objects, as Section 3 showed. Copying does not automatically make every level independent.
Treat inputs, returned values and side effects as three separate parts of the design. A function can return None and still have changed data. A function can return a number without changing its inputs.
def with_bonus(scores, bonus):
return [score + bonus for score in scores]
original = [8, 5]
updated = with_bonus(original, 2)
print(original)
print(updated)EXPECTED OUTPUT
[8, 5] [10, 7]
09 | COMPARE LOCAL REBINDING AND SHARED CHANGE
The model begins with caller list [8, 5]. Choose an operation and trace what remains visible after the call. Each run starts afresh, so you can compare effects directly.
10 | DESIGN CALLS YOU CAN EXPLAIN
For each function, identify the parameters, arguments, locals, returned result and any mutation. Trace an ordinary call and a second call with different inputs. For optional lists, test omitted input and an explicitly supplied empty list separately.
Do not fix a scope error by adding global everywhere. Ask which code should own the value. Passing it explicitly and returning a result often produces a smaller, clearer dependency between functions.
A clean function boundary helps the next lesson: when you move a function into another file, it should receive what it needs rather than relying on a local variable that exists only in the importing caller.
PUT YOUR SKILLS TO WORK
Use the Section 4 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 4 challenges Python course navigation