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...
UNIT 1 : CH 3
Dec 09, 2025
โ๏ธ Working with Python Function
Learning Support
Need Help With This Chapter?
Save key topics for exam revision, ask questions to teachers, or submit content corrections.
Verified Doubts & Teacher Answers
Loading resolved questions for this note...