# Function Scope and Argument Types

> Chapter 14 of the Courseiva PYTHON-PCEP curriculum — https://courseiva.com/learn/python-pcep/function-scope-and-arguments

**Official objective:** 5.2 — Understand local vs global scope, positional and keyword arguments.

## Introduction

Function Scope and Argument Types. They solve a deceptively simple problem: how does Python decide which variable names a function can see, and how does a caller hand data into a function without causing chaos? For the PCEP-30-02 exam you need to understand that variables are not just floating around — they live in specific neighbourhoods (scopes) and arguments arrive at functions like addressed packages, either with a label (keyword) or by position order.

## The Cook's Recipe Box Analogy

Have you ever watched a cook in a bustling restaurant kitchen, calmly pulling ingredients from a labeled drawer while the chef at the next station frantically searches the whole room for salt?

That is exactly the difference between local scope and global scope in Python. In the kitchen, each cook has their own small drawer right at their station. That drawer is the local scope. It holds the spices and tools that cook needs for their current dish. Other cooks cannot just grab from that drawer because it belongs to that station. When a function runs in Python, it creates its own little drawer of variable names. Those variable names only exist inside that function. They are local. If a different function tries to use that same name, it will get a NameError, just like the chef from the next station reaching into your drawer and finding nothing.

But there is also the pantry. The pantry is global scope. Any cook can grab a jar of salt from the pantry. Global variables in Python are like that pantry salt — they are accessible to every function in the program. But a good cook rarely uses the pantry for their main ingredients because it is too far and everyone fights over the same jar. Imagine the chaos if every function changed the same global variable! That would be like one cook turning the pantry's salt into sugar without telling anyone. Scope keeps things organised, safe, and predictable.

## Core explanation

Let us start with the most important idea: scope is the region of your program where a variable name is visible and can be used. Python follows a rule called LEGB (Local, Enclosing, Global, Built-in). For the PCEP-30-02 exam, you mainly need to understand Local and Global scopes.

When you write a variable inside a function, that variable is born when the function runs and it dies when the function ends. That is local scope. If you define a variable at the top level of your script, outside any function, it has global scope. That variable lives for the entire run of the program.

Here is a classic trap. You cannot modify a global variable inside a function without using the 'global' keyword. If you try to assign a value to a variable name inside a function, Python assumes you are creating a new local variable, even if a global variable with the same name exists outside. This causes a UnboundLocalError if you try to read the variable before you assign it, because the local variable is not yet defined.

Now let us talk about how arguments get into functions. There are two types you must memorise for the exam: positional arguments and keyword arguments.

- Positional arguments: the order matters. The first value you pass goes into the first parameter, the second into the second parameter, and so on. If you have a function 'def greet(name, greeting)', calling 'greet("Alice", "Hello")' puts "Alice" into name and "Hello" into greeting. Mix up the order and you get unexpected results.

- Keyword arguments: you specify the parameter name explicitly. Calling 'greet(greeting="Hi", name="Bob")' works even though the order is swapped, because you used the keyword. The Python interpreter matches the keyword to the parameter name.

You can mix both types, but there is a hard rule: all positional arguments must come before any keyword arguments. If you write 'greet("Charlie", greeting="Hey")', that works. But 'greet(name="David", "Good morning")' will cause a SyntaxError because "Good morning" is a positional argument that comes after a keyword argument.

Why does this matter? Because functions are how you package reusable logic. Without a clear way to pass data in (arguments) and a clear rule for which variables the function can see (scope), large programs would be impossible to debug. The scope rule protects you from accidentally changing a variable that another part of your program relies on. Argument types let you write flexible functions that can accept inputs in a readable order.

There is one more nuance: default parameter values. You can define a parameter with a default value, like 'def power(base, exponent=2)'. If the caller omits the exponent, it defaults to 2. Default parameters are evaluated only once, when the function is defined — not each time it is called. That matters for mutable defaults like lists, but that is beyond PCEP-30-02. For now, just know that default values are assigned at definition time.

Finally, remember that changing a mutable object passed as an argument (like a list) inside a function will affect the original list outside. But reassigning the parameter name to a new object will not. This is often called 'pass by object reference'. Python does not pass a copy; it passes a reference to the actual object. If you modify the object through the reference, the caller sees the change. If you assign a new object to the parameter, the reference is lost locally and the original outside stays intact.

## Real-world context

Imagine you work in a small e-commerce company's IT support team. Your manager asks you to write a Python script that calculates discounts for customer orders. This is a perfect scenario to apply function scope and argument types in a realistic way.

You start with a global variable 'TAX_RATE = 0.08' because the tax rate applies to every function. You also have a global list 'order_history' that logs every discount applied.

Your first task is to write a function 'calculate_discount(order_total, customer_tier)'. This function needs to accept the order total as a positional argument and the customer tier (like 'gold' or 'silver') as a keyword argument because you want to make the tier optional and default to 'bronze'. Inside the function, you check the customer_tier and calculate the discount. You also need to append a record of this discount to the global 'order_history' list. Since you are modifying the list (not reassigning it), you do not need the 'global' keyword to append to 'order_history'. But if you accidentally tried 'order_history = []' inside the function, you would be creating a new local variable, not clearing the global one. That would be a bug.

Now your manager introduces a new requirement: they want a function that logs a message every time a certain condition is met, but they realise they accidentally used a common variable name 'log_count' both at the global level and inside the function. Without understanding scope, you would cause a UnboundLocalError because the function tries to increment 'log_count' but Python sees a local assignment and does not look for the global. The fix is to either rename one or use 'global log_count' inside the function.

Later, you are asked to write a function that formats a shipping label. The function 'create_label(first_name, last_name, city, country="USA")' is called many times with positional arguments for the customer names and a keyword argument only when the country differs. You can call 'create_label("Jane", "Doe", "London", country="UK")' and it works perfectly. But if someone calls 'create_label(city="Paris", "Jean", "Dupont")', Python throws a SyntaxError because the positional argument comes after the keyword argument. Your job as a developer is to write documentation and possibly add input validation to prevent such errors.

In practice, you will also use the 'global' keyword sparingly. Experienced developers treat global variables like a shared phonebook — convenient but easy to overwrite by accident. For the exam and real work, prefer passing values as arguments and returning results. This makes your functions pure and testable. When you debug a script with scope issues, you literally step through the code line by line with print statements to see which variable the function sees. That is how IT professionals spend their time: not memorising syntax, but tracing where a variable comes from.

## Exam focus

The PCEP-30-02 exam tests function scope and argument types directly in at least 4-6 questions across the exam. You absolutely must know the following.

First, the LEGB rule. Questions will show you a nested function and ask what value a variable will have. Practise reading code from the inside out. For PCEP, you only need to master Local and Global. Enclosing (closure) and Built-in are tested lightly but understanding the order is crucial.

Second, the difference between modifying a mutable object and reassigning a variable inside a function. They love this trap. They give you a function that modifies a list, and then print the list outside. The answer is that the list changed. Then they give you a function that does 'x = 5' inside, and print 'x' outside, and the answer is whatever the global x was. Memorise this pattern.

Third, keyword-only arguments. You must know that you cannot use a positional argument after a keyword argument. The exam will show a code snippet with mixed argument types and ask whether it is valid. The invalid ones always violate the positional-before-keyword rule.

Fourth, default parameter values. They will write a function with a default parameter and call it with zero arguments. You must know that the default value is used. But they may also test that default values are evaluated only once at definition time (though this is more of a PCPP-30-02 topic, PCEP might touch it lightly).

Fifth, the 'global' keyword. They will show a function that tries to modify a global variable without using 'global' and ask what happens. The correct answer is a UnboundLocalError. They will also show a function that uses 'global x' and then modifies x, and ask what the global value is after the function call.

Here is the list of exact concepts and traps:

- Local vs global scope: variable visibility
- UnboundLocalError: reading a local variable before assignment when a global variable with the same name exists
- 'global' statement: when to use it
- Positional arguments: order matters
- Keyword arguments: named params, order independent
- Mixing positional and keyword: positional must come first
- Default parameter values: assigned at definition
- Mutable argument modifications: changes persist outside the function

The trap patterns are: they always put a print statement after a function that modifies a global via side effect (list append), and they expect you to notice the change. They also love to put a variable name inside a function that shadows a global, then the function tries to read it before assigning it — that is an immediate UnboundLocalError.

To memorise: 'Every variable has a scope. If you assign to it inside a function, it becomes local unless you say global. Arguments are passed by object reference. Mutable objects can be changed inside the function. Order of arguments: positional, then keyword.'

## Step by step

1. **Define a function with parameters** — Write 'def my_function(param1, param2):'. The parameters are local variable names that will hold the incoming argument values. They are only visible inside the function body.
2. **Call the function with positional arguments** — Write 'my_function(10, 20)'. Python assigns 10 to param1 and 20 to param2 purely by position. If you swap them, param1 gets 20. This is the simplest calling method.
3. **Call the function with keyword arguments** — Write 'my_function(param2=20, param1=10)'. The order no longer matters because Python uses the parameter names written after the equals sign to match the values. This improves readability for functions with many parameters.
4. **Mix positional and keyword arguments** — Write 'my_function(10, param2=20)'. The positional argument (10) is first and matches param1. The keyword argument (param2=20) is second. This is valid because all positional arguments come before all keyword arguments.
5. **Use a global variable inside the function** — Write 'global x' at the top of the function body, then assign to 'x'. Without that line, 'x = 5' inside the function would create a new local variable named x, leaving the global x unchanged. The global statement tells Python to use the global scope for that name.

## Comparisons

### Local Variable vs Global Variable

**Local Variable:**
- Defined inside a function body
- Only exists while the function runs
- Cannot be accessed outside the function

**Global Variable:**
- Defined at the top level of a script
- Exists for the entire program run
- Accessible from any function (with or without 'global' keyword to modify)

### Positional Argument vs Keyword Argument

**Positional Argument:**
- Matched to parameters by position in the call
- Order must match the function definition
- Cannot be used after a keyword argument in the same call

**Keyword Argument:**
- Matched to parameters by parameter name
- Order does not matter
- Must appear after all positional arguments in the call

### Modifying a Mutable Argument (e.g., list) vs Reassigning a Parameter

**Modifying a Mutable Argument (e.g., list):**
- Changes the original object outside the function
- No 'global' keyword needed for this operation
- Example: my_list.append(5)

**Reassigning a Parameter:**
- Does not affect the original variable outside
- Creates a new local binding for the parameter name
- Example: my_list = [1,2,3] inside the function

## Diagram

_Flowchart showing the relationship between function definitions, local variable creation, and the LEGB variable lookup order in Python._

```mermaid
flowchart TD
    A[Function Definition] --> B[Parameters are local names]
    A --> C[Body executes]
    C --> D{Variable assignment inside function?}
    D -- Yes --> E[Creates local variable unless 'global' keyword used]
    D -- No --> F[Looks up variable via LEGB rule]
    F --> G[Local scope -- inside current function]
    F --> H[Enclosing scope -- outer functions]
    F --> I[Global scope -- top level of script]
    F --> J[Built-in scope -- Python's builtins]
    E --> K[Local variable only exists while function runs]
    G --> K
    H --> K
    I --> L[Global variable accessible everywhere]
    J --> M[Always accessible]
```

## Common misconceptions

- **Misconception:** If I pass a variable to a function, the function gets a copy, so changing it inside the function won't affect the original. **Reality:** Python passes a reference to the object, not a copy. If the object is mutable (like a list), changes inside the function affect the original. If you reassign the parameter name, the original variable is untouched. (People often carry the 'pass by value' mental model from other languages (or assume it works like that). The exam tests this by having a function append to a list and then checking the list outside.)
- **Misconception:** Using the same variable name inside a function automatically refers to the global variable. **Reality:** If you assign a value to a variable name inside a function, Python creates a new local variable. To modify the global variable, you must use the 'global' keyword. (It seems intuitive that a variable with the same name would be the same variable. But Python's design chooses safety: local names are always local by default unless explicitly stated.)
- **Misconception:** Keyword arguments can be placed anywhere in the function call, including before positional arguments. **Reality:** All positional arguments must come before any keyword arguments in a function call. Putting a keyword argument first causes a SyntaxError. (Python's grammar is strict here. Beginners often think that because keyword arguments are named, order shouldn't matter at all. But the interpreter cannot know where the positional arguments end if keywords interrupt the sequence.)
- **Misconception:** Default values are evaluated fresh each time the function is called. **Reality:** Default values are evaluated only once, when the function is defined, not each time it is called. For immutable defaults like numbers and strings, this makes no practical difference. For mutable defaults like lists, it causes surprising behaviour. (The phrase 'default value' implies the value is set each time you call the function. But Python attaches the default to the function object at definition time. This is counterintuitive and a favourite exam trick.)
- **Misconception:** If a function changes a global list using 'append', I need the 'global' keyword. **Reality:** The 'global' keyword is only needed when you are reassigning the variable name itself (e.g., 'my_list = []'). Modifying the object that the name refers to (like appending to a list) does not require 'global' because you are not changing what the name points to. (Beginners think 'global' controls all access to a global variable. In reality, it only controls assignment to the name. Reading and mutating the object are allowed without it.)

## Key takeaways

- A variable assigned inside a function is local by default and cannot be seen outside that function.
- To modify a global variable inside a function, you must declare it with the 'global' keyword first.
- Positional arguments are matched to parameters by order; keyword arguments are matched by name.
- In a function call, all positional arguments must appear before any keyword arguments.
- Default parameter values are set once at function definition time, not each time the function is called.
- If a mutable object like a list is passed as an argument and modified inside the function, the change persists outside the function.

## FAQ

**What is the difference between local and global scope in Python?**

Local scope refers to variables defined inside a function — they only exist while the function runs. Global scope refers to variables defined at the top level of a script, outside any function, and they are accessible everywhere. A function can read a global variable but cannot change its value without using the 'global' keyword.

**Why do I get UnboundLocalError when I try to use a global variable inside a function?**

You get UnboundLocalError because Python sees you assigning to a variable name inside the function and therefore treats it as a local variable. If you then try to read that same name before assigning a value to it, Python raises UnboundLocalError. To fix it, either don't assign to the name or use the 'global' keyword.

**Can I use keyword arguments before positional arguments in a function call?**

No. Python's syntax requires that all positional arguments appear before any keyword arguments in a function call. If you put a keyword argument first, you will get a SyntaxError. This is a strict rule you must remember for the exam.

**When should I use global variables instead of passing arguments?**

Almost never. Global variables make code harder to debug because any part of the program can change them. Use arguments to pass data into functions and return values to get data out. The only common exception is for constants (like a tax rate or a configuration value) that are truly read-only.

**Why does changing a list inside a function affect the original list outside?**

Because Python passes the reference to the object, not a copy of the object. When you call a function with a list argument, the parameter holds the same reference. If you modify the list (e.g., with append), you are modifying the same object that exists outside the function. If you reassign the parameter to a new list, the original list is unaffected.

**What is the LEGB rule in Python?**

LEGB stands for Local, Enclosing, Global, Built-in. It is the order Python uses to look up variable names. It first checks the local scope of the current function, then the enclosing scopes of any outer functions, then the global scope, and finally the built-in scope. For the PCEP-30-02 exam, you primarily need to understand Local and Global.

## Check your understanding

1. **What is the scope of a variable that is defined at the top level of a Python script, outside any function?**

   Answer: Global scope. It is accessible from anywhere in the script, including inside functions, but can only be modified inside a function if the 'global' keyword is used.

2. **What error occurs when you try to read a variable inside a function before assigning to it, and a global variable with the same name exists?**

   Answer: UnboundLocalError. Python treats the variable as local because there is an assignment somewhere in the function, but the local variable has not been assigned a value yet.

3. **In a function call, can you place a keyword argument before a positional argument?**

   Answer: No. All positional arguments must appear before any keyword arguments. Doing otherwise causes a SyntaxError.

4. **If you pass a list to a function and inside the function you use 'append' on that list, does the original list outside change?**

   Answer: Yes. Python passes a reference to the object, so modifying the mutable object (like appending to a list) changes the original list outside the function.

5. **What happens if you call a function with fewer arguments than it has parameters, and one of the parameters has a default value?**

   Answer: The missing parameter uses its default value. If a parameter without a default value is omitted, Python raises a TypeError.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/python-pcep/function-scope-and-arguments
