Functions are the **backbone of every real Python program**. Master this chapter and you unlock the power to write clean, reusable, professional code โ and score full marks in board exams!
> [!TIP]
> **Study Strategy:** Read a section โ study the code โ cover the output and predict it yourself โ check. For board prep, every `> [!IMPORTANT]` box is a guaranteed exam topic. Sections **3.5**, **3.8**, and **3.9** are the examiner's absolute favourites โ give them extra time!
---
## ๐ Chapter at a Glance
::: grid
::: card 3.1โ3.2 | Foundations | What functions are, types, and how to call them | Built-in vs User-defined
::: card 3.3โ3.4 | Defining & Flow | `def` syntax, program structure, execution flow | Parameters vs Arguments
::: card 3.5 | Passing Parameters | Positional, Default, Keyword, Mixed โ all 4 types | โญ Exam hotspot
::: card 3.6โ3.7 | Return & Composition | Single/multiple return values, chaining functions | Tuple packing/unpacking
::: card 3.8 | Scope & LEGB | Local vs Global, name resolution, `global` keyword | โญ Exam hotspot
::: card 3.9 | Mutable/Immutable | How data type affects what happens inside functions | โญ Exam hotspot
:::
---
## 3.1 ๐ Introduction
Imagine you're writing a report card program. You need to calculate a student's grade at 50 different places. Would you paste the same 10 lines **fifty times**? ๐ฑ
That's exactly the problem **functions** solve.
> A **function** is a named, reusable block of code that performs a specific task. Define it once โ call it anywhere, any number of times.
::: grid
::: card ๐ | Reusability | Write once, call anywhere โ eliminate all repetition | `calc_grade()` called 50 times from 1 definition
::: card ๐งฉ | Modularity | Divide a huge program into focused, bite-sized pieces | `login()` โ `dashboard()` โ `logout()`
::: card ๐ | Easy Debugging | A bug in `calc_tax()`? Fix it once โ fixed everywhere | No more hunting through 50 identical code blocks
::: card ๐ | Readability | Good function names make code read like plain English | `get_student_marks()` โ no explanation needed!
:::
> [!IMPORTANT]
> **๐ Board Exam Tip โ 2 Marks**
> "List any four advantages of using functions in Python."
> **Always write:** (1) Reusability โ code is defined once, used multiple times. (2) Modularity โ complex problems broken into smaller parts. (3) Easier debugging โ fix a bug in one place. (4) Avoids repetition โ follows the DRY (Don't Repeat Yourself) principle.
---
## 3.2 ๐ง Understanding Functions
### 3.2.1 Calling / Invoking / Using a Function
To **execute** a function, you write its name followed by parentheses. This is called a **function call**.
```
Syntax: function_name() โ no arguments
function_name(arg1, arg2, ...) โ with arguments
```
### Code Example: calling_functions.py
```python
# calling_functions.py
# โโ DEFINITION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def wish_student():
print("๐ Welcome to Chapter 3: Functions!")
print(" You're going to ace your board exam!")
# โโ CALLS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
wish_student() # First call
wish_student() # Second call โ SAME function, REUSED!
wish_student() # Third call โ defined once, called three times โ
# OUTPUT (printed 3 times):
# ๐ Welcome to Chapter 3: Functions!
# You're going to ace your board exam!
```
> [!NOTE]
> **What happens when Python sees a function call?**
> It immediately **jumps** to the function definition, executes every statement inside (top to bottom), then **returns** to the exact line after the call and continues the program. Think of it as pressing "detour" โ you always come back!
---
### 3.2.2 Python Function Types
Python organises functions into **two main categories:**
::: grid
::: card ๐ญ | Built-in Functions | Always available โ no import needed. Pre-loaded when Python starts. | `print()` `input()` `len()` `type()` `int()` `float()` `str()` `abs()` `max()` `min()` `sum()` `range()`
::: card ๐ ๏ธ | User-defined Functions | Created by YOU for a specific purpose using the `def` keyword | `calc_area()` `find_grade()` `is_prime()` `get_average()`
:::
**Built-in Functions โ Quick Reference:**
| Function | Purpose | Example | Output |
| :--- | :--- | :--- | :--- |
| `print()` | Display output | `print("Hi")` | `Hi` |
| `input()` | Read input (always returns string) | `input("Name: ")` | user's text |
| `len()` | Length of a sequence | `len([1,2,3])` | `3` |
| `type()` | Check data type | `type(3.14)` | `<class 'float'>` |
| `int()` | Convert to integer | `int("18")` | `18` |
| `abs()` | Absolute value | `abs(-9)` | `9` |
| `max()` / `min()` | Largest / smallest | `max(3, 7, 1)` | `7` |
| `sum()` | Sum of iterable | `sum([1,2,3])` | `6` |
| `range()` | Generate number sequence | `range(1, 6)` | `1 2 3 4 5` |
> [!IMPORTANT]
> **๐ Board Exam Tip โ 2 Marks**
> "What are the two types of functions in Python? Give examples of each."
> Name both types, define them, and give **at least 2 built-in examples**. `print()` and `len()` are the safest choices.
---
## 3.3 ๐ง Defining Functions in Python
Creating your own function uses the `def` keyword. This is called **defining** or **declaring** a function.
```
def function_name ( parameter1, parameter2 ) :
โ โ โ โ
def name you variables to receive colon is
keyw. choose input values MANDATORY!
"""Docstring โ optional, explains the function"""
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ function body โ โ MUST be indented (4 spaces)
โ statement 1 โ
โ statement 2 โ
โ return value (optional) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### Code Example: function_types_demo.py
```python
# function_types_demo.py
# TYPE A: No parameters, no return value
def print_divider():
"""Prints a decorative line"""
print("=" * 40)
# TYPE B: With parameters, no return value
def greet(name, city):
"""Greets a student"""
print(f"Hello {name} from {city}! ๐")
# TYPE C: No parameters, with return value
def get_pi():
"""Returns the value of pi"""
return 3.14159
# TYPE D: With parameters, with return value (most common!)
def square(num):
"""Returns the square of a number"""
return num * num
# โโ CALLING ALL FOUR TYPES โโโโโโโโโโโโโโโโโโโโโโ
print_divider() # TYPE A
greet("Arjun", "Durgapur") # TYPE B
pi = get_pi() # TYPE C
result = square(9) # TYPE D
print(f"ฯ = {pi}") # ฯ = 3.14159
print(f"9ยฒ = {result}") # 9ยฒ = 81
print_divider()
```
> [!WARNING]
> **Function Naming Rules โ Memorise These!**
> โ
Must start with a **letter** or underscore `_` โ never a digit
> โ
Only letters, digits, underscores โ **no spaces, hyphens, or special chars**
> โ
Cannot be a keyword: `def`, `return`, `if`, `for`, `while`, etc.
> โ
Case-sensitive: `Square` โ `square` โ `SQUARE`
> `calc_area` โ
| `_helper` โ
| `findMax` โ
| `2greet` โ | `my-func` โ | `return` โ
---
### 3.3.1 Structure of a Python Program
Every well-written Python program follows this **four-layer structure:**
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ LAYER 1 โ Import Statements โ
โ import math โ
โ import random โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ LAYER 2 โ Global Constants & Variables โ
โ SCHOOL_NAME = "DPS Durgapur" โ
โ MAX_MARKS = 100 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ LAYER 3 โ Function Definitions โ
โ def calc_percentage(marks): โ
โ return (marks / MAX_MARKS) * 100 โ
โ โ
โ def print_report(name, pct): โ
โ print(f"{name}: {pct}%") โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ LAYER 4 โ Main Program (function calls) โ
โ name = input("Student name: ") โ
โ marks = int(input("Marks: ")) โ
โ pct = calc_percentage(marks) โ
โ print_report(name, pct) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
> [!NOTE]
> This structure is not enforced by Python โ it's a **professional convention**. Functions must be defined **before** they are called. Writing them in Layer 3 and calling them in Layer 4 guarantees this order.
---
## 3.4 ๐ Flow of Execution in a Function Call
Understanding exactly **how** Python moves through a function call is the key to understanding all of Chapter 3.
```
MAIN PROGRAM FUNCTION DEFINITION
โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโ
def add(a, b):
โ x = 10 โข result = a + b โ parameters receive values
โก y = 20 โฃ return result โ sends value back
โ
โค total = add(x, y) โโโโโโโโโโกโโโบ โ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โค 30 returned
โฅ print(total) โ 30
```
**Step-by-step walkthrough:**
| Step | Location | What Happens |
| :---: | :--- | :--- |
| โ | Main program | `x = 10`, `y = 20` assigned |
| โก | Main program | Python hits `add(x, y)` โ a function call! |
| โข | Jump โ function | Parameters `a = 10`, `b = 20` (values copied in) |
| โฃ | Inside function | `result = 10 + 20 = 30` computed |
| โค | Inside function | `return result` โ value `30` sent back to caller |
| โฅ | Back in main | `total = 30`; `print(30)` executes |
### Code Example: flow_demo.py
```python
# flow_demo.py โ watch the execution flow live
def add(a, b):
print(f" โข Inside add(): a={a}, b={b}")
result = a + b
print(f" โฃ Returning: {result}")
return result
print("โ Starting main program")
x, y = 10, 20
print("โก Calling add(x, y)...")
total = add(x, y) # โ jumps to function definition
print(f"โฅ Back in main. total = {total}")
# โ Starting main program
# โก Calling add(x, y)...
# โข Inside add(): a=10, b=20
# โฃ Returning: 30
# โฅ Back in main. total = 30
```
---
### 3.4.1 Arguments and Parameters
This distinction trips up many students. Here it is, crystal clear:
::: grid
::: card ๐ | Parameter | **Variable** in the function **definition** | `def add(a, b):` โ `a` and `b` are **parameters**
::: card ๐ฆ | Argument | Actual **value** passed during **function call** | `add(10, 20)` โ `10` and `20` are **arguments**
:::
```python
def add(a, b): # a, b โ PARAMETERS (placeholders in definition)
return a + b
add(10, 20) # 10, 20 โ ARGUMENTS (actual values in call)
```
> [!TIP]
> **Memory Trick โ PAD**
> **P**arameters live in the **def** line (definition)
> **A**rguments are the **actual** values you pass when calling
> **D**on't mix them up โ examiners love asking this!
> [!IMPORTANT]
> **๐ Board Exam Tip โ 2 Marks**
> "Differentiate between a parameter and an argument with an example."
> **Answer:** A *parameter* is the variable listed inside the parentheses of a function definition. It acts as a placeholder. An *argument* is the actual value passed to the function at the time of calling. Example: In `def add(a, b):`, `a` and `b` are parameters. In `add(10, 20)`, `10` and `20` are arguments.
---
## 3.5 ๐จ Passing Parameters
Python gives you **four powerful ways** to pass values to functions. This entire section is a **board exam goldmine** โ master every type.
---
### 3.5.1 Positional / Required Arguments
The **simplest** method. Arguments are matched to parameters **strictly by their position**. First argument โ first parameter, second โ second, and so on.
```
POSITION: 1st 2nd 3rd
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def describe(name, class_no, section):
โ โ โ
describe( "Priya", 12, "A")
โ โ โ
name="Priya" class_no=12 section="A"
```
### Code Example: positional_args.py
```python
# positional_args.py
def display_student(name, class_no, section):
print(f" Name : {name}")
print(f" Class : {class_no}-{section}")
print()
# โ
Correct โ arguments match parameter order
display_student("Priya", 12, "A")
# Name : Priya
# Class : 12-A
# โ Wrong order โ Python won't error, but output is nonsensical!
display_student(12, "Priya", "A")
# Name : 12 โ name got the integer!
# Class : Priya-A โ class got the string!
```
> [!WARNING]
> **Silent Bug Alert!**
> Python assigns positional arguments by order โ no type checking. If you swap arguments, you get wrong output with **no error message**. These bugs are very hard to find. Always double-check the order of positional arguments!
---
### 3.5.2 Default Arguments
Assign a **default value** to a parameter inside the `def` line. If the caller doesn't provide that argument, the default is used. These parameters become **optional**.
### Code Example: default_args.py
```python
# default_args.py
def book_ticket(name, destination, travel_class="Economy"):
# โ default value set here
print(f" โ๏ธ Passenger : {name}")
print(f" Destination: {destination}")
print(f" Class : {travel_class}")
print()
# โโ Without optional argument โ default kicks in โโโโโโโโโโ
book_ticket("Arjun", "Mumbai")
# โ๏ธ Passenger : Arjun
# Destination: Mumbai
# Class : Economy โ default used automatically
# โโ With optional argument โ default overridden โโโโโโโโโโโ
book_ticket("Priya", "Delhi", "Business")
# โ๏ธ Passenger : Priya
# Destination: Delhi
# Class : Business โ default replaced by "Business"
# โโ Multiple defaults โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def create_profile(name, age=16, city="Unknown", grade="B"):
print(f" {name} | Age:{age} | {city} | Grade:{grade}")
create_profile("Rahul") # all defaults
create_profile("Ananya", 17) # age overridden
create_profile("Vikram", 17, "Kolkata", "A+") # all provided
```
> [!IMPORTANT]
> **๐ Board Exam Tip โ THE Golden Rule of Defaults**
> **Default parameters MUST always come AFTER all required parameters.**
> ```python
> def func(a, b=10, c=20): # โ
Required first, defaults after
> def func(a=10, b, c): # โ SyntaxError โ default before required!
> def func(a, b=5, c): # โ SyntaxError โ required after default!
> ```
> This is a **1-mark guaranteed error-spotting question** every year. Burn this rule into memory.
> [!NOTE]
> **Why does this rule exist?**
> If a default parameter came first, Python wouldn't know whether a given argument was intended for it or the required parameter after it โ complete ambiguity. The rule makes every function call unambiguous.
---
### 3.5.3 Keyword (Named) Arguments
You explicitly **name** the parameter when calling the function. Because you name it, **order no longer matters!**
### Code Example: keyword_args.py
```python
# keyword_args.py
def calc_volume(length, breadth, height):
vol = length * breadth * height
print(f" {length}L ร {breadth}B ร {height}H = {vol} cu.cm")
# โโ Positional (order matters) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
calc_volume(10, 5, 3) # 150 cu.cm
# โโ Keyword (order doesn't matter!) โโโโโโโโโโโโโโโโโโโโโโโ
calc_volume(height=3, length=10, breadth=5) # same: 150 cu.cm
calc_volume(breadth=5, height=3, length=10) # same: 150 cu.cm
# โโ Mix: positional first, then keyword โโโโโโโโโโโโโโโโโโโ
calc_volume(10, height=3, breadth=5) # โ
length=10 positionally
# calc_volume(10, breadth=5, 3) # โ SyntaxError!
```
```
POSITIONAL CALL: calc_volume(10, 5, 3)
โ โ โ
length breadth height (matched by position)
KEYWORD CALL: calc_volume(height=3, length=10, breadth=5)
matched by NAME โ position irrelevant!
```
> [!IMPORTANT]
> **๐ Board Exam Tip โ 2 Marks**
> "Distinguish between positional and keyword arguments."
> **Answer:** In *positional arguments*, values are matched to parameters by their order of appearance. In *keyword arguments*, each value is preceded by the parameter name (using `=`), so order doesn't matter. Example: `func(a=10, b=5)` is a keyword call; `func(10, 5)` is a positional call.
---
### 3.5.4 Using Multiple Argument Types Together
All three types can be combined in one function call โ but strict **ordering rules** apply.
```
IN DEFINITION: required params โ default params
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def f(a, b, c=10, d=20):
โ โ โ โ
required defaults
IN A CALL: positional args โ keyword args
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
f(1, 2, d=30)
โ โ โ
positional keyword
```
### Code Example: mixed_args.py
```python
# mixed_args.py
def student_report(name, roll_no, section='A', percentage=75.0):
# req. req. default default
print(f" Name : {name}")
print(f" Roll No. : {roll_no}")
print(f" Section : {section}")
print(f" Percentage : {percentage}%")
print()
# โโ All required, all defaults kick in โโโโโโโโโโโโโโโโโโโโ
student_report("Riya Sharma", 101)
# section='A', percentage=75.0 used
# โโ Override section (positional) โโโโโโโโโโโโโโโโโโโโโโโโโ
student_report("Amit Kumar", 102, 'B')
# section='B', percentage=75.0 used
# โโ Skip section default, set only percentage (keyword) โโโ
student_report("Neha Gupta", 103, percentage=91.5)
# section='A' (default), percentage=91.5 (keyword override)
# โโ All explicit using keyword args โโโโโโโโโโโโโโโโโโโโโโโ
student_report("Raj Singh", 104, section='C', percentage=87.0)
```
**The Three Rules โ Know These for Exams:**
| # | Rule | โ
Correct | โ Wrong |
| :---: | :--- | :--- | :--- |
| 1 | Default after required **in definition** | `def f(a, b=10)` | `def f(a=10, b)` |
| 2 | Positional before keyword **in call** | `f(5, b=10)` | `f(a=5, 10)` |
| 3 | Once keyword used, all following must be keyword | `f(5, b=10, c=20)` | `f(5, b=10, 20)` |
> [!IMPORTANT]
> **๐ Board Exam Tip โ 3 Marks**
> Examiners give a function definition with mixed argument types and ask you to identify valid/invalid calls. Practice these three rules until they're automatic.
---
## 3.6 ๐ค Returning Values From Functions
The `return` statement is the function's way of sending a result back to the caller. It does **two things at once:**
```
return expression
โ โ
Exits the Value sent back
function to the caller
immediately
```
> Any code written **after** `return` in the same function is **dead code** โ it will never execute.
### Code Example: return_demo.py
```python
# return_demo.py
def find_grade(marks):
"""Returns the letter grade for given marks"""
if marks >= 90:
return 'A+' # โ exits immediately; no elif/else needed!
if marks >= 80:
return 'A'
if marks >= 70:
return 'B'
if marks >= 60:
return 'C'
return 'D' # โ default case
# โโ Using the returned value โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
score = 85
grade = find_grade(score)
print(f"Score: {score} โ Grade: {grade}") # Score: 85 โ Grade: A
# โโ Return value used directly in an expression โโโโโโโโโโโ
print(f"Your grade: {find_grade(72)}") # Your grade: B
# โโ No return statement โ returns None โโโโโโโโโโโโโโโโโโโโ
def show_message(msg):
print(msg) # does something but returns nothing
result = show_message("Hello!") # prints "Hello!"
print(result) # None โ automatic!
print(type(result)) # <class 'NoneType'>
```
> [!IMPORTANT]
> **๐ Board Exam Tip โ 1 Mark**
> "What value does a Python function return if it has no `return` statement?"
> **Answer:** It returns `None` (of type `NoneType`). This happens automatically. A bare `return` with no expression also returns `None`.
---
### 3.6.1 Returning Multiple Values
Python functions can return **more than one value** in a single `return` statement. Python **automatically packs** them into a **tuple**.
```
return a, b, c
โ
(a, b, c) โ tuple created automatically
```
### Code Example: multi_return.py
```python
# multi_return.py
def circle_stats(radius):
"""Returns area AND circumference together"""
import math
area = round(math.pi * radius ** 2, 2)
circum = round(2 * math.pi * radius, 2)
return area, circum # โ packed into tuple (area, circum)
# โโ METHOD 1: Unpack into individual variables (preferred) โ
area, circum = circle_stats(7)
print(f"Area : {area} sq.units") # 153.94
print(f"Circumference : {circum} units") # 43.98
# โโ METHOD 2: Capture entire tuple โโโโโโโโโโโโโโโโโโโโโโโโโ
stats = circle_stats(7)
print(type(stats)) # <class 'tuple'>
print(stats) # (153.94, 43.98)
print(stats[0]) # 153.94 โ access by index
# โโ Real-world: 4-value return โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def arithmetic(a, b):
"""Returns all four basic operations"""
return a+b, a-b, a*b, a/b
add, sub, mul, div = arithmetic(20, 4)
print(f"+ {add} - {sub} ร {mul} รท {div}")
# + 24 - 16 ร 80 รท 5.0
```
> [!TIP]
> **Memory Trick: "Pack & Unpack"**
> Python **packs** multiple return values โ `return a, b, c` becomes `(a, b, c)`
> You **unpack** by matching variable count โ `x, y, z = func()` extracts each value
> If counts don't match: `ValueError: too many values to unpack`
> [!IMPORTANT]
> **๐ Board Exam Tip โ 3 Marks**
> "Can a Python function return more than one value? How? Give an example."
> **Answer:** Yes. Python packs multiple return values into a tuple automatically. Use `return val1, val2` in the function. Capture with either `t = func()` (as a tuple) or `a, b = func()` (unpacked into separate variables). Show both methods in your answer.
---
## 3.7 ๐ Composition
**Function composition** means using the **return value of one function directly as the argument to another function** โ all in a single expression. The inner function executes first.
```
result = outer( inner(x) )
โโโโโโโโโโ
executes first
โโโโโโโโโโโโโโโโโโโโโโ
receives inner's result
```
### Code Example: composition.py
```python
# composition.py
def square(x):
return x * x # xยฒ
def double(x):
return 2 * x # 2x
def add_ten(x):
return x + 10 # x + 10
# โโ Simple composition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
result = double(square(3))
# Step 1: square(3) = 9
# Step 2: double(9) = 18
print(result) # 18
# โโ Triple composition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
result2 = add_ten(double(square(2)))
# Step 1: square(2) = 4
# Step 2: double(4) = 8
# Step 3: add_ten(8) = 18
print(result2) # 18
# โโ Real-world: ยฐF โ ยฐC โ K in two composed steps โโโโโโโโโ
def f_to_c(f):
return (f - 32) * 5 / 9
def c_to_k(c):
return c + 273.15
kelvin = c_to_k(f_to_c(98.6)) # 98.6ยฐF directly to Kelvin
print(f"98.6ยฐF = {kelvin}K") # 98.6ยฐF = 310.15K
```
> [!NOTE]
> **Readability Over Cleverness**
> `f(g(h(x)))` works but gets confusing. For readability, break it up:
> ```python
> step1 = h(x)
> step2 = g(step1)
> result = f(step2)
> ```
> Both give identical results โ choose what's clearest for your reader.
---
## 3.8 ๐ญ Scope of Variables
**Scope** is the region of a program where a variable **exists and is accessible**. Python has strict scope rules โ a variable defined in one place may be completely invisible in another.
::: grid
::: card ๐ | Local Scope | Declared **inside** a function | Exists only during function execution; destroyed when function ends
::: card ๐ | Global Scope | Declared **outside** all functions | Exists throughout the entire program's lifetime
:::
### Code Example: scope_basics.py
```python
# scope_basics.py
school = "DPS Durgapur" # โ GLOBAL โ lives at module level
def show_student():
student = "Arjun" # โ LOCAL โ lives only inside this function
print(f"Student : {student}") # โ
Local accessible here
print(f"School : {school}") # โ
Global accessible here too
show_student()
print(school) # โ
Global accessible outside functions
# print(student) # โ NameError โ local 'student' doesn't exist here!
```
**Scope Comparison Table:**
| Feature | ๐ Local Variable | ๐ Global Variable |
| :--- | :--- | :--- |
| **Declared inside** | A function | Module level (outside all functions) |
| **Accessible from** | Only within that function | Anywhere in the program |
| **Created when** | Function is called | Program starts |
| **Destroyed when** | Function finishes | Program ends |
| **Modify inside function** | Directly | Requires `global` keyword |
---
### 3.8.1 Name Resolution โ The LEGB Rule
When Python encounters a name, it **searches for it** in this precise order โ stopping the moment it's found:
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SEARCH ORDER โ
โ โ
โ L โโโ E โโโ G โโโ B โ
โ โ โ โ โ โ
โ Local Enclosing Global Built-in โ
โ(fn body)(outer fn)(module)(Python) โ
โ โ
โ If found โ use it. If not found anywhere โ NameError โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
::: grid
::: card ๐ต L โ Local | Inside the **current** function body | `def f(): x = 1` โ `x` searched here first
::: card ๐ฃ E โ Enclosing | Inside any **outer** function (for nested functions) | `def outer(): x=1; def inner(): ...`
::: card ๐ข G โ Global | At **module level** โ top of the file, outside all functions | `x = 10` written at file level
::: card ๐ก B โ Built-in | Python's **pre-defined** names, always available | `print`, `len`, `range`, `int`, `True`, `None`
:::
### Code Example: legb_demo.py
```python
# legb_demo.py โ LEGB in action
x = "๐ Global" # G scope
def outer():
x = "๐ฃ Enclosing" # E scope
def inner():
x = "๐ต Local" # L scope
print(f"inner sees: {x}") # Finds L first
inner()
print(f"outer sees: {x}") # Finds E (no L in outer's scope)
outer()
print(f"module sees: {x}") # Finds G
# inner sees: ๐ต Local
# outer sees: ๐ฃ Enclosing
# module sees: ๐ Global
```
**The `global` Keyword โ Write Back to Global Scope:**
### Code Example: global_keyword.py
```python
# global_keyword.py
visitors = 0 # Global counter
def enter_site():
global visitors # โ "I mean the global 'visitors', not a new local one"
visitors = visitors + 1
def reset_counter():
global visitors
visitors = 0
enter_site()
enter_site()
enter_site()
print(f"Visitors after 3 entries : {visitors}") # 3
reset_counter()
print(f"Visitors after reset : {visitors}") # 0
```
> [!IMPORTANT]
> **๐ Board Exam Tip โ Classic Error Question**
> This code appears in almost every board exam as an error-spotting question:
> ```python
> count = 10
> def increment():
> count = count + 1 # โ UnboundLocalError!
> increment()
> ```
> **Why it fails:** The assignment `count = ...` makes Python treat `count` as *local* to `increment()`. But then `count + 1` tries to read it before it's been assigned locally โ contradiction!
> **Fix:** Add `global count` as the **first line** of the function.
> [!WARNING]
> **Avoid Global Variable Abuse!**
> Every function that uses `global` can secretly change a value that every other part of your program relies on. Debugging becomes a nightmare. **Best practice:** pass values as arguments and return results โ it's safer, cleaner, and more professional.
---
## 3.9 ๐ Mutable / Immutable Properties of Passed Data Objects
This topic is conceptually the deepest in the chapter โ and examiners **love** setting output questions on it.
**Core question:** When you pass a variable to a function, does modifying it inside the function affect the original outside?
**The answer depends entirely on the data type.**
::: grid
::: card ๐ Immutable | Cannot be changed in place | `int` `float` `str` `tuple` `bool` โ changes inside a function do NOT affect the original
::: card ๐ Mutable | Can be changed in place | `list` `dict` `set` โ changes inside a function DO affect the original
:::
---
### 3.9.1 Mutability/Immutability of Arguments/Parameters and Function Calls
#### ๐ Case 1 โ Immutable Object (int, float, str, tuple)
When you modify an immutable object inside a function, Python **creates a brand-new object** and points the local variable to it. The original is completely untouched.
```
BEFORE CALL:
num โโโโโโโโโโโโโโโโโโโบ [ 5 ] (integer object in memory)
INSIDE FUNCTION (x = x + 100):
x โโโโโโโโโโโโโโโโโโโบ [ 5 ] โ same object initially
โ
x = x + 100 โ NEW object created
x โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ [ 105 ] โ x moves to new object
AFTER CALL:
num โโโโโโโโโโโโโโโโโโโบ [ 5 ] โ ORIGINAL UNTOUCHED!
```
### Code Example: immutable_pass.py
```python
# immutable_pass.py
def modify_int(x):
print(f" Entry : x={x}, id={id(x)}")
x = x + 100 # new object created
print(f" Exit : x={x}, id={id(x)}") # different id!
num = 5
print(f"Before call : num={num}, id={id(num)}")
modify_int(num)
print(f"After call : num={num}, id={id(num)}") # UNCHANGED!
# Before call : num=5, id=94...
# Entry : x=5, id=94... โ same id โ same object
# Exit : x=105, id=13... โ different id โ new object!
# After call : num=5, id=94... โ original intact
```
#### ๐ Case 2 โ Mutable Object (list, dict, set)
When you modify a mutable object inside a function, **the same object in memory** is changed. Both the parameter inside and the variable outside point to the same object.
```
BEFORE CALL:
scores โโโโโโโโโโโโโโโโบ [ 70, 80, 85 ] (list object)
INSIDE FUNCTION (.append(95)):
marks_list โโโโโโโโโโโโบ [ 70, 80, 85 ] โ SAME object!
marks_list.append(95) โ modifies in place
marks_list โโโโโโโโโโโโบ [ 70, 80, 85, 95 ]
AFTER CALL:
scores โโโโโโโโโโโโโโโโบ [ 70, 80, 85, 95 ] โ ORIGINAL CHANGED!
(scores and marks_list pointed to the SAME list all along)
```
### Code Example: mutable_pass.py
```python
# mutable_pass.py
def update_scores(marks_list):
print(f" Entry : {marks_list}, id={id(marks_list)}")
marks_list.append(95) # modifies the SAME list
marks_list[0] = 100 # also modifies in place
print(f" Exit : {marks_list}, id={id(marks_list)}") # same id!
scores = [70, 80, 85]
print(f"Before call : {scores}, id={id(scores)}")
update_scores(scores)
print(f"After call : {scores}") # CHANGED โ [100, 80, 85, 95]
# id is identical throughout โ only ONE list exists in memory
```
#### ๐ Side-by-Side Comparison:
### Code Example: mutable_vs_immutable.py
```python
# mutable_vs_immutable.py
def try_change_string(s): # str โ IMMUTABLE
s = "Changed!"
print(f" Inside (str): {s}")
def try_change_list(lst): # list โ MUTABLE
lst.append("Added!")
print(f" Inside (lst): {lst}")
# โโ IMMUTABLE TEST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
name = "Rahul"
print(f"String before call : {name}")
try_change_string(name)
print(f"String after call : {name}") # Rahul โ unchanged โ
print()
# โโ MUTABLE TEST โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
subjects = ["Maths", "Science"]
print(f"List before call : {subjects}")
try_change_list(subjects)
print(f"List after call : {subjects}") # ['Maths', 'Science', 'Added!'] โ changed!
```
**Complete Summary:**
| Property | ๐ Immutable (int, float, str, tuple) | ๐ Mutable (list, dict, set) |
| :--- | :--- | :--- |
| Changed in place? | โ No โ new object created | โ
Yes โ same object modified |
| Affects original outside function? | โ No | โ
Yes |
| `id()` changes on modification? | โ
Yes (new object) | โ No (same object) |
| Safe to pass carelessly? | โ
Yes | โ ๏ธ Be careful! |
| Examples | `int`, `float`, `str`, `tuple`, `bool` | `list`, `dict`, `set` |
> [!IMPORTANT]
> **๐ Board Exam Tip โ Most Commonly Appearing Output Question!**
> ```python
> def modify(x, y):
> x = x + 10
> y.append(10)
>
> a = 5
> b = [1, 2, 3]
> modify(a, b)
> print(a, b)
> ```
> **Answer: `5 [1, 2, 3, 10]`**
> `a` โ `int` (immutable) โ `x = x + 10` creates new local object โ original `a` untouched โ still `5`
> `b` โ `list` (mutable) โ `y.append(10)` modifies same object โ original `b` changed โ `[1, 2, 3, 10]`
> **This exact pattern repeats every board year. Memorise the explanation, not just the answer.**
> [!TIP]
> **Protecting a mutable object from unintended modification:**
> ```python
> modify(a, b.copy()) # pass a copy โ b is safe
> # OR
> modify(a, list(b)) # list() creates a fresh copy too
> ```
> Bonus trap: `lst = lst + [100]` inside a function creates a **NEW list** (doesn't affect original), while `lst.append(100)` modifies **in place** (does affect original). A very common tricky question!
---
## โ ๏ธ Common Errors โ Error-Spotting Made Easy
| # | Error | Wrong Code โ | Corrected Code โ
| Error Type |
| :---: | :--- | :--- | :--- | :--- |
| 1 | Call before definition | `greet()` โ then `def greet():` | `def greet():` โ then `greet()` | `NameError` |
| 2 | Wrong number of arguments | `add(5)` for `def add(a, b)` | `add(5, 3)` | `TypeError` |
| 3 | Default before required | `def f(x=1, y):` | `def f(y, x=1):` | `SyntaxError` |
| 4 | Keyword before positional | `f(a=5, 10)` | `f(10, a=5)` | `SyntaxError` |
| 5 | Modify global without declaration | `count = count + 1` in fn | Add `global count` first | `UnboundLocalError` |
| 6 | Assuming immutable changes | Expect `num` changes after `modify(num)` | `int` won't change outside | Logic error (no Python error) |
---
## ๐ Quick Revision โ Exam Ready in 5 Minutes!
**Function Basics**
- `def` โ defines a function | `()` after name โ calls it
- Parameters (definition) โ Arguments (call)
- Always define before calling
- No `return` โ returns `None` automatically
**Argument Types**
| Type | Where | Key Rule | Example |
| :--- | :--- | :--- | :--- |
| Positional | Call | Order is everything | `func(10, 20)` |
| Keyword | Call | Name it โ order free | `func(b=20, a=10)` |
| Default | Definition | Must come after required | `def func(a, b=10):` |
**Scope โ LEGB Search Order**
```
Local โ Enclosing โ Global โ Built-in (first match wins)
```
Use `global var` inside a function to modify a global variable.
**Mutability in Functions**
- `int`, `float`, `str`, `tuple` โ **Immutable** โ original **never** changes
- `list`, `dict`, `set` โ **Mutable** โ original **always** changes if modified in-place
**Multiple Return Values**
```python
return val1, val2, val3 # auto-packed into a tuple
a, b, c = func() # unpack back into variables
```
---
## ๐ฏ Board Exam Questions โ Fully Solved
### Q1: Output question [2 marks]
```python
def func(a, b=5, c=10):
print(a, b, c)
func(1)
func(1, 2)
func(1, 2, 3)
func(c=3, a=1)
```
**Output:**
```
1 5 10
1 2 10
1 2 3
1 5 3
```
---
### Q2: Error finding and correction [2 marks]
```python
def student(marks=0, name):
print(name, marks)
student("Rahul", 90)
```
**Error:** `SyntaxError` โ default parameter `marks` is placed before required parameter `name`.
**Corrected code:**
```python
def student(name, marks=0): # required param first
print(name, marks)
student("Rahul", 90) # Output: Rahul 90
```
---
### Q3: Scope output question [3 marks]
```python
x = 10
def outer():
x = 20
def inner():
x = 30
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
```
**Output:**
```
inner: 30
outer: 20
global: 10
```
**Explanation:** Each function has its own local `x`. LEGB โ `inner()` finds its own `x=30` first. `outer()` finds its own `x=20`. The global `x=10` is never modified.
---
### Q4: Write a function [3 marks]
Write a Python function `find_stats(numbers)` that accepts a list and returns the minimum, maximum, and average.
```python
def find_stats(numbers):
"""Returns min, max, and average of a number list"""
minimum = min(numbers)
maximum = max(numbers)
average = sum(numbers) / len(numbers)
return minimum, maximum, round(average, 2)
# Function call with unpacking
data = [85, 92, 78, 96, 88, 73]
lo, hi, avg = find_stats(data)
print(f"Min: {lo} | Max: {hi} | Average: {avg}")
# Min: 73 | Max: 96 | Average: 85.33
```
---
### Q5: Tricky mutability question [2 marks]
```python
def change(val, lst):
val = val * 2
lst = lst + [100]
num = 5
arr = [1, 2, 3]
change(num, arr)
print(num, arr)
```
**Answer:** `5 [1, 2, 3]`
**Explanation:**
- `val = val * 2` โ creates a new local integer โ `num` unchanged โ still `5`
- `lst = lst + [100]` โ **creates a NEW list** and assigns it to local `lst` โ the original `arr` is NOT modified (this is different from `lst.append(100)` which modifies in place!)
- So `arr` is also unchanged โ still `[1, 2, 3]`
*(Key trap: `lst + [100]` โ new object. `lst.append(100)` โ same object. Very different!)*
---
## ๐ช Practice Problems
Solve these in order โ they build on each other:
1. Write `is_prime(n)` โ returns `True` if `n` is prime, `False` otherwise
2. Write `reverse_string(s)` โ returns the reversed string (no slicing allowed โ use a loop)
3. Write `count_vowels(sentence)` โ returns the count of vowels (a, e, i, o, u)
4. Write `find_grade(marks)` โ returns grade A+/A/B/C/D, then use it with composition: `print(find_grade(get_marks()))`
5. Write three nested functions demonstrating all of L, E, G in the LEGB rule with the same variable name `x`
6. Write `swap_elements(lst, i, j)` โ swaps two elements of a list; show that the original list is modified
7. Write `power(base, exp=2)` โ `power(4)` returns `16`, `power(2, 10)` returns `1024`
8. Write `divmod_custom(a, b)` โ returns both quotient and remainder using multiple return values
Back to List
Calculating...