Python has several concepts that look difficult at first, but they become simple once we understand what Python is actually doing.
This guide covers:
- LEGB Scoping Rule
- Local, Enclosing, Global and Built-in scopes
nonlocal- First-Class Functions
*args**kwargs- Packing and Unpacking
- Mutable Default Argument Pitfall
- Safe ways to use default arguments
1. LEGB Scoping Rule
What is Scope?
Scope means the area of a program where a variable can be accessed.
For example:
def greet():
name = "Deepika"
print(name)
greet()
Here, name is created inside greet(), so it belongs to the local scope of that function.
Python follows a specific rule to find variables. This rule is called the LEGB rule.
- L → Local
- E → Enclosing
- G → Global
- B → Built-in
Python searches for a variable in this order:
Local
↓
Enclosing
↓
Global
↓
Built-in
Python stops searching as soon as it finds the variable.
2. L → Local Scope
A variable created inside a function is usually a local variable.
Example
def greet():
name = "Deepika"
print(name)
greet()
Here, name = "Deepika" is local to greet().
It cannot normally be accessed outside the function:
def greet():
name = "Deepika"
print(name)
This gives a NameError because name only exists inside greet().
Simple Definition: Local scope is the scope inside the current function.
3. E → Enclosing Scope
The enclosing scope appears when one function is defined inside another function.
Example
def outer():
name = "Deepika"
def inner():
print(name)
inner()
outer()
Here, name is not inside inner(). It is inside outer().
Therefore, from the point of view of inner(), name is in the enclosing scope.
outer()
│
├── name = "Deepika"
│
└── inner()
│
└── print(name)
Simple Definition: Enclosing scope is the scope of an outer function surrounding the current inner function.
This concept is especially important when learning closures.
4. G → Global Scope
A variable created outside all functions is generally in the global scope.
Example
name = "Deepika"
def greet():
print(name)
greet()
Python cannot find name inside greet(), so it looks in the global scope and finds it.
Local → Not found
Enclosing → Not found
Global → Found!
Output:
Deepika
Simple Definition: Global scope is the scope outside functions and classes at the module level.
5. B → Built-in Scope
Python already provides many names that we can use directly.
Examples: print(), len(), sum(), max(), min(), type()
These names belong to Python's built-in scope.
Example
numbers = [10, 20, 30]
print(len(numbers))
For len, Python searches:
Local → Not found
Enclosing → Not found
Global → Not found
Built-in → Found!
Output:
3
Simple Definition: Built-in scope contains names that are provided by Python itself.
6. Complete LEGB Example
x = "Global"
def outer():
x = "Enclosing"
def inner():
x = "Local"
print(x)
inner()
outer()
Output:
Local
Why? Because Python searches Local first and finds it there. It stops immediately and does not continue searching the enclosing, global, or built-in scopes.
7. Another LEGB Example
x = "Global"
def outer():
x = "Enclosing"
def inner():
print(x)
inner()
outer()
Output:
Enclosing
Why? Python searches:
Local → Not found
Enclosing → Found!
So it uses "Enclosing".
8. nonlocal
nonlocal is used when an inner function wants to modify a variable belonging to an enclosing function.
Example
def outer():
count = 0
def inner():
nonlocal count
count += 1
inner()
print(count)
outer()
Output:
1
Here, nonlocal count tells Python: "count belongs to the enclosing function. I want to modify that variable."
Simple Definition:
nonlocaltells Python to use a variable from the enclosing function's scope instead of creating a new local variable.
When is nonlocal useful? It is commonly used with:
- Nested functions
- Closures
- Functions that need to remember and update state
9. First-Class Functions
One of the most important things about Python is:
Functions are objects too.
Because functions are objects, they can be treated like other values. A function can be:
- Assigned to a variable
- Passed as an argument
- Returned from another function
- Stored in a list
- Stored in a dictionary
- Used later
This is called first-class functions.
10. Assigning a Function to a Variable
def greet():
print("Hello!")
x = greet
x()
Output:
Hello!
Here, x = greet means x now refers to the greet function.
Important Difference
-
greetmeans: the function itself. -
greet()means: call or execute the function.
So x = greet stores the function, but x = greet() calls the function immediately and stores its return value.
11. Passing a Function as an Argument
Because functions are first-class objects, we can pass a function to another function.
def greet():
print("Hello!")
def execute(function):
function()
execute(greet)
Output:
Hello!
Here, execute(greet) passes the greet function to execute(). Inside execute(), function() calls the function.
The flow is:
greet
↓
passed to execute()
↓
stored in parameter "function"
↓
function()
↓
Hello!
12. Returning a Function
A function can also return another function.
def outer():
def inner():
print("Hello!")
return inner
x = outer()
x()
Output:
Hello!
Here, x = outer() stores the returned inner function in x. Then x() calls inner().
This idea is very important for understanding closures and decorators.
13. When Are First-Class Functions Useful?
First-class functions are useful when we want to:
- Pass behavior to another function
- Create callbacks
- Build decorators
- Create closures
- Choose a function dynamically
- Store multiple functions
- Execute functions later
Simple Idea
Normally, we pass data:
process(10)
But because functions are objects, we can also pass behavior:
process(greet)
14. *args
Sometimes we don't know how many positional arguments a function will receive.
def add(a, b):
return a + b
This function expects two arguments: add(10, 20). But this will cause an error:
add(10, 20, 30, 40)
If we want the function to accept any number of positional arguments, we can use *args.
Syntax
def function_name(*args):
...
Example
def show(*args):
print(args)
show(10, 20, 30)
Output:
(10, 20, 30)
The arguments are collected into a tuple:
args = (10, 20, 30)
15. Example Using *args
def add(*args):
total = 0
for number in args:
total += number
return total
print(add(10, 20))
print(add(10, 20, 30))
print(add(10, 20, 30, 40))
Output:
30
60
100
Simple Definition:
*argsallows a function to accept any number of positional arguments and collects them into a tuple.
16. Is args a Special Keyword?
No. The * is what matters. The name can technically be anything:
def show(*numbers):
print(numbers)
But Python programmers normally use *args because it is the standard convention.
17. **kwargs
Now let's talk about keyword arguments.
def student(name, age):
print(name)
print(age)
We can call this function using keyword arguments:
student(name="Deepika", age=21)
But what if we don't know how many keyword arguments will be provided? We can use **kwargs.
Syntax
def function_name(**kwargs):
...
Example
def student(**kwargs):
print(kwargs)
student(name="Deepika", age=21, city="Bangalore")
Output:
{'name': 'Deepika', 'age': 21, 'city': 'Bangalore'}
The keyword arguments are collected into a dictionary:
kwargs = {
"name": "Deepika",
"age": 21,
"city": "Bangalore"
}
18. Simple Definition of **kwargs
**kwargsallows a function to accept any number of keyword arguments and collects them into a dictionary.
19. *args vs **kwargs
| Feature | *args |
**kwargs |
|---|---|---|
| Accepts | Positional arguments | Keyword arguments |
| Stores data as | Tuple | Dictionary |
| Example | 10, 20, 30 |
name="Deepika" |
| Symbol | * |
** |
The easiest way to remember:
*args → Positional arguments → Tuple
**kwargs → Keyword arguments → Dictionary
20. Using *args and **kwargs Together
We can use both in the same function.
def demo(*args, **kwargs):
print(args)
print(kwargs)
demo(
10,
20,
30,
name="Deepika",
age=21
)
Output:
(10, 20, 30)
{'name': 'Deepika', 'age': 21}
Python separates them: 10, 20, 30 → *args → Tuple, and name="Deepika", age=21 → **kwargs → Dictionary.
21. Packing and Unpacking
The * and ** symbols can also be used for unpacking. There are two different ideas: Packing and Unpacking.
22. Packing
When defining a function:
def demo(*args):
print(args)
*args collects multiple positional arguments into one tuple. This is called packing.
demo(10, 20, 30)
becomes conceptually:
args = (10, 20, 30)
23. Unpacking with *
Suppose we have a list:
numbers = [10, 20, 30]
def add(a, b, c):
return a + b + c
We can do:
print(add(*numbers))
This is equivalent to:
print(add(10, 20, 30))
The * takes the elements from the list and passes them as separate positional arguments. This is called unpacking.
24. Dictionary Unpacking with **
Suppose we have:
student = {
"name": "Deepika",
"age": 21
}
def show(name, age):
print(name)
print(age)
We can do:
show(**student)
This is equivalent to:
show(name="Deepika", age=21)
The ** unpacks the dictionary into keyword arguments.
25. Packing vs Unpacking
PACKING
↓
Many values → One variable
Example: def show(*args):
UNPACKING
↓
One collection → Many arguments
Example: show(*numbers)
For dictionaries:
-
**kwargspacks keyword arguments into a dictionary. -
function(**data)unpacks a dictionary into keyword arguments.
26. Mutable Default Argument Pitfall
Now let's look at one of Python's famous pitfalls.
A default argument is a value given to a parameter when the caller doesn't provide one.
def greet(name="Deepika"):
print("Hello", name)
greet()
Output:
Hello Deepika
Here, name="Deepika" is the default value.
27. What Is a Mutable Object?
A mutable object is an object whose contents can be changed. Common mutable types include list, dict, set.
numbers = []
numbers.append(10)
print(numbers)
Output:
[10]
The list was modified.
28. The Mutable Default Argument Problem
Consider this function:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("Apple"))
print(add_item("Banana"))
print(add_item("Mango"))
Output:
['Apple']
['Apple', 'Banana']
['Apple', 'Banana', 'Mango']
This may be surprising. We might expect:
['Apple']
['Banana']
['Mango']
But that's not what happens.
29. Why Does This Happen?
The important rule is:
Python creates default argument objects when the function is defined, not every time the function is called.
So def add_item(item, items=[]): creates one default list.
Function
|
└── Default list
|
├── First call → ['Apple']
|
├── Second call → ['Apple', 'Banana']
|
└── Third call → ['Apple', 'Banana', 'Mango']
The same list is being reused. Because lists are mutable, changes made to that list remain there.
30. Why Is It Called a "Mutable Default Argument Pitfall"?
-
Mutable — the object can be changed.
[]is a mutable list. -
Default Argument — this is the default parameter:
items=[] - Pitfall — the same mutable object can be reused across function calls.
Simple Definition: The mutable default argument pitfall occurs when a mutable object such as a list, dictionary, or set is used as a default parameter and modified, causing its changes to persist between function calls.
31. The Wrong Way
Avoid this when you want a fresh list for every function call:
def add_item(item, items=[]):
items.append(item)
return items
32. The Safe Way: Use None
Instead, use:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item("Apple"))
print(add_item("Banana"))
print(add_item("Mango"))
Output:
['Apple']
['Banana']
['Mango']
Perfect!
33. Why Does None Fix the Problem?
We use items=None as a signal meaning: "The caller did not provide a list."
Then if items is None: items = [] creates a new list when needed.
So instead of:
Call 1 ─┐
Call 2 ─┼──→ SAME LIST
Call 3 ─┘
we get:
Call 1 → NEW LIST
Call 2 → NEW LIST
Call 3 → NEW LIST
34. Mutable Default Dictionaries
The same problem can happen with dictionaries.
Avoid:
def add_user(name, users={}):
users[name] = "active"
return users
Prefer:
def add_user(name, users=None):
if users is None:
users = {}
users[name] = "active"
return users
35. Mutable Default Sets
The same idea applies to sets.
Avoid:
def add_number(number, numbers=set()):
numbers.add(number)
return numbers
Prefer:
def add_number(number, numbers=None):
if numbers is None:
numbers = set()
numbers.add(number)
return numbers
36. Are All Default Arguments Dangerous?
No. The problem specifically concerns mutable objects that are modified.
Common immutable types include: int, float, str, tuple, bool, None.
def counter(count=0):
count += 1
return count
print(counter())
print(counter())
print(counter())
Output:
1
1
1
This is fine because integers are immutable.
37. The Safe Pattern to Remember
Whenever you want a fresh mutable object for every function call, use None.
List
def function(data=None):
if data is None:
data = []
Dictionary
def function(data=None):
if data is None:
data = {}
Set
def function(data=None):
if data is None:
data = set()
This pattern is extremely common in real Python code.
38. Quick Revision
LEGB
L → Local
E → Enclosing
G → Global
B → Built-in
LEGB is Python's rule for searching for a variable name.
nonlocal
nonlocal variable
nonlocaltells an inner function to use and modify a variable from its enclosing function.
First-Class Functions
Functions are objects in Python, so they can be assigned to variables, passed as arguments, returned from functions, and stored in collections.
def greet():
print("Hello")
x = greet
x()
*args
def function(*args):
*argsaccepts any number of positional arguments and stores them in a tuple.
def show(*args):
print(args)
show(10, 20, 30)
Output: (10, 20, 30)
`kwargs`**
def function(**kwargs):
**kwargsaccepts any number of keyword arguments and stores them in a dictionary.
def show(**kwargs):
print(kwargs)
show(name="Deepika", age=21)
Output: {'name': 'Deepika', 'age': 21}
Mutable Default Argument
Avoid:
def function(items=[]):
Prefer:
def function(items=None):
if items is None:
items = []
A mutable default argument can cause changes to persist between function calls because the default object is created when the function is defined and can be reused.
39. Final Cheat Sheet
LEGB
L → Local
E → Enclosing
G → Global
B → Built-in
Python searches in this order.
First-Class Functions
Functions are objects.
They can be:
• Assigned to variables
• Passed as arguments
• Returned from functions
• Stored in collections
*args
Many positional arguments
↓
Tuple
`kwargs`**
Many keyword arguments
↓
Dictionary
Mutable Default Argument
Avoid:
def func(items=[]):
Prefer:
def func(items=None):
if items is None:
items = []
One-Minute Memory
LEGB → Where does Python search for a variable?
Local → Enclosing → Global → Built-in
First-Class Function → Function can be treated like an object.
*args → Many positional arguments → Tuple
**kwargs → Many keyword arguments → Dictionary
*list → Unpack list/sequence → Positional arguments
**dictionary → Unpack dictionary → Keyword arguments
Mutable Default → Avoid [] / {} / set() as defaults → Use None → Create a fresh object inside the function
These concepts are important foundations for understanding closures, decorators, callbacks, scope, function arguments, and advanced Python programming.
Top comments (0)