Dev.to · 12 min read

Python Scope, First-Class Functions, *args,**kwargs & Mutable Default Arguments

Python Scope, First-Class Functions, *args,**kwargs & Mutable Default Arguments

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: nonlocal tells 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 greet means: 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: *args allows 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 **kwargs allows 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: **kwargs packs 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 nonlocal tells 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): *args accepts 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): **kwargs accepts 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.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News