Class 12 Computer Science Chapter 03 · 31 min read

⚙️ Working with Python Function

Unit 1 · Ch 3 Oct 27, 2026
Chapter Quiz Share

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.

💡 Study Strategy

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

3.1–3.2

Foundations

What functions are, types, and how to call them

Built-inModule & User-defined
3.3–3.4

Defining & Flow

def syntax, program structure, execution flow

Parameters vs Arguments
3.5

Passing Parameters

Positional, Default, Keyword, Mixed — all 4 types

⭐ Exam hotspot
3.6–3.7

Return & Composition

Single/multiple return values, chaining functions

Tuple packing/unpacking
3.8

Scope & LEGB

Local vs Global, name resolution, global keyword

⭐ Exam hotspot
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.

🔁

Reusability

Write once, call anywhere — eliminate all repetition

calc_grade() called 50 times from 1 definition
🧩

Modularity

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 blocks
📖

Readability

Good function names make code read like plain English

get_student_marks() — no explanation needed!
📋 📋 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

# 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!
🧠 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 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
💡 Two ways to import

import math → call it as math.sqrt(16).
from math import sqrt → call it directly as sqrt(16).

📋 📋 Board Exam Tip — 2 Marks

"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()
⚠ 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)                         │
└──────────────────────────────────────────────────┘
🧠 Memory Trick

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 def
📦

Argument

Actual value passed during the function call — in add(10, 20): 10 and 20 are arguments

Passed in the call
def add(a, b):         # a, b → PARAMETERS (placeholders in definition)
    return a + b

add(10, 20)            # 10, 20 → ARGUMENTS (actual values in call)
💡 Memory Trick — PAD

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.

📋 📋 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

# 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!
⚠ 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

# 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
📋 📋 Board Exam Tip — THE Golden Rule of Defaults

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.

🧠 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

# 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!
📋 📋 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

# 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)
📋 📋 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

# 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'>
📋 📋 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

# 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
💡 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

📋 📋 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

# 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
🧠 Readability Over Cleverness

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 ends
🌍

Global Scope

Declared outside all functions

Exists throughout the entire program's lifetime

Code 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 first
🟣

E — 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 level
🟡

B — Built-in

Python's pre-defined names, always available

printlenrangeintTrueNone

Code 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
📋 📋 Board Exam Tip — Classic Error Question

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.

⚠ 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 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

intfloatstrtuplebool
🔓

Mutable

Can be changed in place — changes inside a function DO affect the original

listdictset

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

# 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
📋 📋 Board Exam Tip — Output Question

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.

💡 Protecting a mutable object from unintended modification

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)

🎯 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 → 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
Take the Chapter 03 Quiz Custom Quiz
Solved Board Questions

Practice real board exam questions for this chapter.

View Solved Board Questions
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

Chapter 03 Quiz