Functions are the building blocks of most real Python programs. This chapter explains how to define and call functions, how control flows during a function call, how to pass parameters and return values, the scope of variables (the LEGB rule), and what happens when mutable and immutable objects are passed to a function.
Read a section → study the code → cover the output and predict it yourself → check. For board prep, read every > box carefully. Sections 3.5, 3.8, and 3.9 need extra time.
📋 Chapter at a Glance
Foundations
What functions are, types, and how to call them
Built-inModule & User-definedDefining & Flow
def syntax, program structure, execution flow
Parameters vs ArgumentsPassing Parameters
Positional, Default, Keyword, Mixed — all 4 types
⭐ Exam hotspotReturn & Composition
Single/multiple return values, chaining functions
Tuple packing/unpackingScope & LEGB
Local vs Global, name resolution, global keyword
⭐ Exam hotspotMutable/Immutable
How data type affects what happens inside functions
⭐ Exam hotspot3.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.
Reusability
Write once, call anywhere — eliminate all repetition
calc_grade() called 50 times from 1 definitionModularity
Divide a huge program into focused, bite-sized pieces
login() → dashboard() → logout()Easy Debugging
A bug in calc_tax()? Fix it once — fixed everywhere
No more hunting through 50 identical code blocksReadability
Good function names make code read like plain English
get_student_marks() — no explanation needed!
"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
# 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!
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 three types:
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()Functions defined in Modules
Stored in a module (a .py file of ready-made functions). Available after you import the module
math.sqrt() math.pow() random.randint() statistics.mean()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 |
Functions defined in Modules — Quick Reference:
A module is a .py file that holds ready-made functions. First import the module, then call its function as module_name.function_name().
import math
print(math.sqrt(49)) # 7.0
print(math.pow(2, 3)) # 8.0
import random
print(random.randint(1, 6)) # any whole number from 1 to 6
from statistics import mean
print(mean([2, 4, 6])) # 4
import math → call it as math.sqrt(16).
from math import sqrt → call it directly as sqrt(16).
"What are the different types of functions in Python? Give examples of each."
Name all three types (built-in, functions defined in modules, user-defined), define each, and give 2 examples of each: print(), len(); math.sqrt(), random.randint(); and any function you write with def.
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
# 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()
✅ 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) │
└──────────────────────────────────────────────────┘
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
# 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:
Parameter
Variable in the function definition — in def add(a, b): a and b are parameters
Defined with defArgument
Actual value passed during the function call — in add(10, 20): 10 and 20 are arguments
Passed in the calldef add(a, b): # a, b → PARAMETERS (placeholders in definition)
return a + b
add(10, 20) # 10, 20 → ARGUMENTS (actual values in call)
Parameters live in the def line (definition)
Arguments are the actual values you pass when calling
Don't mix them up — know this difference clearly.
"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
# 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!
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
# 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
Default parameters MUST always come AFTER all required parameters.
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 typical 1-mark error-spotting question. Learn this rule well.
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
# 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!
"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
# 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) |
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
returnin the same function is dead code — it will never execute.
Code Example: return_demo.py
# 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'>
"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
# 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
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
"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
# 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
f(g(h(x))) works but gets confusing. For readability, break it up:
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.
Local Scope
Declared inside a function
Exists only during function execution; destroyed when function endsGlobal Scope
Declared outside all functions
Exists throughout the entire program's lifetimeCode Example: scope_basics.py
# 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 │
└─────────────────────────────────────────────────────────┘
L — Local
Inside the current function body
def f(): x = 1 → x searched here firstE — Enclosing
Inside any outer function (for nested functions)
def outer(): x=1; def inner(): ...G — Global
At module level — top of the file, outside all functions
x = 10 written at file levelB — Built-in
Python's pre-defined names, always available
printlenrangeintTrueNoneCode Example: legb_demo.py
# 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
# 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
This code is a typical error-spotting question:
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.
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 a good source of output questions.
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.
Immutable
Cannot be changed in place — changes inside a function do NOT affect the original
intfloatstrtupleboolMutable
Can be changed in place — changes inside a function DO affect the original
listdictset3.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
# 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
# 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
# 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 |
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]
Memorise the explanation, not just the answer.
modify(a, b.copy()) # pass a copy — b is safe
# OR
modify(a, list(b)) # list() creates a fresh copy too
Bonus trap question: what is the output?
def add_new(lst):
lst = lst + [100] # makes a NEW list
def add_same(lst):
lst.append(100) # changes the SAME list
a = [1, 2]
add_new(a)
print(a)
add_same(a)
print(a)
Answer:
[1, 2] → lst = lst + [100] creates a new list inside the function, so a is not changed.
[1, 2, 100] → lst.append(100) modifies the original list in place.
Careful: lst += [100] also changes the list in place, just like append().
⚠️ 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) |
Function Basics
def→ defines a function |()after name → calls it- Parameters (definition) ≠ Arguments (call)
- Always define before calling
- No
return→ returnsNoneautomatically
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 changeslist,dict,set→ Mutable → original always changes if modified in-place
Multiple Return Values
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]
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]
def student(marks=0, name):
print(name, marks)
student("Rahul", 90)
Error: SyntaxError — default parameter marks is placed before required parameter name.
Corrected code:
def student(name, marks=0): # required param first
print(name, marks)
student("Rahul", 90) # Output: Rahul 90
Q3: Scope output question [3 marks]
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.
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]
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 →numunchanged → still5lst = lst + [100]→ creates a NEW list and assigns it to locallst— the originalarris NOT modified (this is different fromlst.append(100)which modifies in place!)- So
arris 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:
- Write
is_prime(n)→ returnsTrueifnis prime,Falseotherwise - Write
reverse_string(s)→ returns the reversed string (no slicing allowed — use a loop) - Write
count_vowels(sentence)→ returns the count of vowels (a, e, i, o, u) - Write
find_grade(marks)→ returns grade A+/A/B/C/D, then use it with composition:print(find_grade(get_marks())) - Write three nested functions demonstrating all of L, E, G in the LEGB rule with the same variable name
x - Write
swap_elements(lst, i, j)→ swaps two elements of a list; show that the original list is modified - Write
power(base, exp=2)→power(4)returns16,power(2, 10)returns1024 - Write
divmod_custom(a, b)→ returns both quotient and remainder using multiple return values