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()
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Here, name = "Deepika" is local to greet().
It cannot normally be accessed outside the function:
def greet():
name = "Deepika"
print(name)
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Python cannot find name inside greet(), so it looks in the global scope and finds it.
Local → Not found
Enclosing → Not found
Global → Found!
Enter fullscreen mode Exit fullscreen mode
Output:
Deepika
Enter fullscreen mode Exit fullscreen mode
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))
Enter fullscreen mode Exit fullscreen mode
For len, Python searches:
Local → Not found
Enclosing → Not found
Global → Not found
Built-in → Found!
Enter fullscreen mode Exit fullscreen mode
Output:
3
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Output:
Local
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Output:
Enclosing
Enter fullscreen mode Exit fullscreen mode
Why? Python searches:
Local → Not found
Enclosing → Found!
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Output:
1
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Output:
Hello!
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
Output:
Hello!
Enter fullscreen mode Exit fullscreen mode
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!
Enter fullscreen mode Exit fullscreen mode
12. Returning a Function
A function can also return another function.
def outer():
def inner():
print("Hello!")
return inner
x = outer()
x()
Enter fullscreen mode Exit fullscreen mode
Output:
Hello!
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
But because functions are objects, we can also pass behavior:
process(greet)
Enter fullscreen mode Exit fullscreen mode
14. *args
Sometimes we don’t know how many positional arguments a function will receive.
def add(a, b):
return a + b
Enter fullscreen mode Exit fullscreen mode
This function expects two arguments: add(10, 20). But this will cause an error:
add(10, 20, 30, 40)
Enter fullscreen mode Exit fullscreen mode
If we want the function to accept any number of positional arguments, we can use *args.
Syntax
def function_name(*args):
...
Enter fullscreen mode Exit fullscreen mode
Example
def show(*args):
print(args)
show(10, 20, 30)
Enter fullscreen mode Exit fullscreen mode
Output:
(10, 20, 30)
Enter fullscreen mode Exit fullscreen mode
The arguments are collected into a tuple:
args = (10, 20, 30)
Enter fullscreen mode Exit fullscreen mode
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))
Enter fullscreen mode Exit fullscreen mode
Output:
30
60
100
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
We can call this function using keyword arguments:
student(name="Deepika", age=21)
Enter fullscreen mode Exit fullscreen mode
But what if we don’t know how many keyword arguments will be provided? We can use **kwargs.
Syntax
def function_name(**kwargs):
...
Enter fullscreen mode Exit fullscreen mode
Example
def student(**kwargs):
print(kwargs)
student(name="Deepika", age=21, city="Bangalore")
Enter fullscreen mode Exit fullscreen mode
Output:
{'name': 'Deepika', 'age': 21, 'city': 'Bangalore'}
Enter fullscreen mode Exit fullscreen mode
The keyword arguments are collected into a dictionary:
kwargs = {
"name": "Deepika",
"age": 21,
"city": "Bangalore"
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
)
Enter fullscreen mode Exit fullscreen mode
Output:
(10, 20, 30)
{'name': 'Deepika', 'age': 21}
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
*args collects multiple positional arguments into one tuple. This is called packing.
demo(10, 20, 30)
Enter fullscreen mode Exit fullscreen mode
becomes conceptually:
args = (10, 20, 30)
Enter fullscreen mode Exit fullscreen mode
23. Unpacking with *
Suppose we have a list:
numbers = [10, 20, 30]
def add(a, b, c):
return a + b + c
Enter fullscreen mode Exit fullscreen mode
We can do:
print(add(*numbers))
Enter fullscreen mode Exit fullscreen mode
This is equivalent to:
print(add(10, 20, 30))
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
We can do:
show(**student)
Enter fullscreen mode Exit fullscreen mode
This is equivalent to:
show(name="Deepika", age=21)
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
Output:
Hello Deepika
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
Output:
[10]
Enter fullscreen mode Exit fullscreen mode
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"))
Enter fullscreen mode Exit fullscreen mode
Output:
['Apple']
['Apple', 'Banana']
['Apple', 'Banana', 'Mango']
Enter fullscreen mode Exit fullscreen mode
This may be surprising. We might expect:
['Apple']
['Banana']
['Mango']
Enter fullscreen mode Exit fullscreen mode
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']
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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"))
Enter fullscreen mode Exit fullscreen mode
Output:
['Apple']
['Banana']
['Mango']
Enter fullscreen mode Exit fullscreen mode
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 ─┘
Enter fullscreen mode Exit fullscreen mode
we get:
Call 1 → NEW LIST
Call 2 → NEW LIST
Call 3 → NEW LIST
Enter fullscreen mode Exit fullscreen mode
34. Mutable Default Dictionaries
The same problem can happen with dictionaries.
Avoid:
def add_user(name, users={}):
users[name] = "active"
return users
Enter fullscreen mode Exit fullscreen mode
Prefer:
def add_user(name, users=None):
if users is None:
users = {}
users[name] = "active"
return users
Enter fullscreen mode Exit fullscreen mode
35. Mutable Default Sets
The same idea applies to sets.
Avoid:
def add_number(number, numbers=set()):
numbers.add(number)
return numbers
Enter fullscreen mode Exit fullscreen mode
Prefer:
def add_number(number, numbers=None):
if numbers is None:
numbers = set()
numbers.add(number)
return numbers
Enter fullscreen mode Exit fullscreen mode
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())
Enter fullscreen mode Exit fullscreen mode
Output:
1
1
1
Enter fullscreen mode Exit fullscreen mode
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 = []
Enter fullscreen mode Exit fullscreen mode
Dictionary
def function(data=None):
if data is None:
data = {}
Enter fullscreen mode Exit fullscreen mode
Set
def function(data=None):
if data is None:
data = set()
Enter fullscreen mode Exit fullscreen mode
This pattern is extremely common in real Python code.
38. Quick Revision
LEGB
L → Local
E → Enclosing
G → Global
B → Built-in
Enter fullscreen mode Exit fullscreen mode
LEGB is Python’s rule for searching for a variable name.
nonlocal
nonlocal variable
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode
*args
def function(*args):
Enter fullscreen mode Exit fullscreen mode
*argsaccepts any number of positional arguments and stores them in a tuple.
def show(*args):
print(args)
show(10, 20, 30)
Enter fullscreen mode Exit fullscreen mode
Output: (10, 20, 30)
`kwargs`**
def function(**kwargs):
Enter fullscreen mode Exit fullscreen mode
**kwargsaccepts any number of keyword arguments and stores them in a dictionary.
def show(**kwargs):
print(kwargs)
show(name="Deepika", age=21)
Enter fullscreen mode Exit fullscreen mode
Output: {'name': 'Deepika', 'age': 21}
Mutable Default Argument
Avoid:
def function(items=[]):
Enter fullscreen mode Exit fullscreen mode
Prefer:
def function(items=None):
if items is None:
items = []
Enter fullscreen mode Exit fullscreen mode
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.
Enter fullscreen mode Exit fullscreen mode
First-Class Functions
Functions are objects.
They can be:
• Assigned to variables
• Passed as arguments
• Returned from functions
• Stored in collections
Enter fullscreen mode Exit fullscreen mode
*args
Many positional arguments
↓
Tuple
Enter fullscreen mode Exit fullscreen mode
`kwargs`**
Many keyword arguments
↓
Dictionary
Enter fullscreen mode Exit fullscreen mode
Mutable Default Argument
Avoid:
def func(items=[]):
Prefer:
def func(items=None):
if items is None:
items = []
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
These concepts are important foundations for understanding closures, decorators, callbacks, scope, function arguments, and advanced Python programming.