Class 12Computer Science · Programming with PythonFull chapter

Functions

The whole chapter in one place — read it, then test yourself. Clear notes, a reference sheet, a practice quiz, and worked NCERT solutions & PYQs.

Types of Function

Quick answer Python offers three kinds of function — built-in (ready to use), module (must be imported and called through the module name), and user-defined (written by you with def) — differing only in whether you had to import it and whether you wrote it, not in how you call it.

A function is a named block of code that you write or obtain once and then run many times just by using its name. Python gives you three kinds. The board asks you to name all three, so learn the labels and not merely the idea.

  1. Built-in functions — supplied by Python itself and available the moment the program starts. No import needed. Examples: len(), max(), min(), sum(), round(), type(), int(), print(), input().
  2. Functions defined in a module — they live in a module, which is a library of ready-made code kept outside your program. You must import the module first, then reach the function through the module name. Examples: math.sqrt(), random.randint(), statistics.mean().
  3. User-defined functions — the ones you write yourself with def. Everything from the next section onwards is about these.

Every module on this syllabus comes bundled with Python, so there is nothing to install. Where a module physically sits varies and does not matter to you: statistics and random are ordinary .py files in Python’s library folder, while math has no .py file at all because it is compiled into the interpreter itself. The distinction that does matter is simpler — did you have to import it, and did you write it?

TypeWhere the code sitsImport needed?How you call it
Built-inAlways available in the interpreterNolen(marks)
ModuleA library outside your program, bundled with PythonYesmath.sqrt(144)
User-definedYour own program fileNogst_amount(1200, 18)

Built-in functions first. Note that type() is itself listed among Python’s built-in functions, and it reports what something is.

marks = [78, 92, 65, 88, 55]
print(len(marks))
print(max(marks))
print(min(marks))
print(sum(marks))
print(round(sum(marks) / len(marks), 2))
print(type(marks))

Output:

5
92
55
378
75.6

Now module functions. The random.seed(7) line is there only so that the number printed below is the same one you will get; remove it and randint gives a fresh value on every run.

import math
import random

print(math.sqrt(144))
print(math.floor(87.9), math.ceil(87.1))
print(math.pow(2, 10))

random.seed(7)
print(random.randint(1, 6))

Output:

12.0
87 88
1024.0
3

Two things worth noticing. First, math.sqrt(144) gives 12.0 and not 12, because sqrt and pow hand back floats. Do not turn that into a rule about the whole module, though: math.floor and math.ceil return plain integers, which is exactly why the second line of output reads 87 88 rather than 87.0 88.0. The same is true of math.factorial and math.gcd. Check the one function you are using rather than assuming.

Second, forgetting the import does not produce any special “module missing” error. As far as the program is concerned the name math was simply never created, so you get an ordinary NameError:

print(math.sqrt(144))

Output:

NameError: name 'math' is not defined. Did you forget to import 'math'?

That second sentence is a hint Python 3.12 and later attach to the error. On an older lab machine you will see only NameError: name 'math' is not defined, and that shorter form is what an exam answer should give. Keep it separate in your mind from ModuleNotFoundError: No module named 'xyz', which is what happens when you do write an import but name a module that does not exist.

Finally, a user-defined function. GST at 18% on a purchase of Rs 1200:

def gst_amount(price, rate):
    return price * rate / 100

print(gst_amount(1200, 18))
print(gst_amount(45000, 5))

Output:

216.0
2250.0

One warning before you leave this section, because it is a trap students fall into. type() tells you what kind of object a name holds, but it is not a test of which of the three categories a function belongs to:

import builtins
import math
import statistics

def gst_amount(price, rate):
    return price * rate / 100

print(type(len))
print(type(math.sqrt))
print(type(statistics.mean))
print(type(gst_amount))
print(len(dir(builtins)))

Output:





160

len and math.sqrt both report builtin_function_or_method because both are written in C. But statistics.mean reports function — identical to the gst_amount you wrote yourself — because the statistics module is itself written in ordinary Python. So type() is telling you what language a function was written in, not whether it is built-in, imported or yours. Classify by the table instead.

That last line counts the names Python hands you for free in this version (Python 3.13). The exact figure changes between versions, so never memorise it — the point is that the built-in namespace is a fixed, modest list, and everything else you must either import or write.

Worked example — all three types in one program

A CGPA helper for a Class 12 marksheet: built-ins do the counting, two module functions do the arithmetic, and one function of your own holds the rule that is specific to your school.

import math
import statistics

def cgpa(marks):
    return round(statistics.mean(marks) / 9.5, 2)

marks = [88, 92, 76, 95, 84]

print("count   :", len(marks))
print("highest :", max(marks))
print("mean    :", statistics.mean(marks))
print("sqrt sum:", math.sqrt(sum(marks)))
print("cgpa    :", cgpa(marks))

Output:

count   : 5
highest : 95
mean    : 87
sqrt sum: 20.85665361461421
cgpa    : 9.16

The mean prints as 87 and not 87.0 because the five marks add up to 435, which divides exactly by 5, and statistics.mean keeps an exact whole answer as an int. Look also at what cgpa() does inside its own body: it calls a module function and a built-in. Functions calling other functions is completely normal, and understanding the exact order in which that happens is the subject of the next section.

Import a whole module import math Creates the name math; only then does math.sqrt exist. Without it you get NameError, not a syntax error.
Import selected names from math import sqrt, pi Now call sqrt(x) with no prefix. Careful: this does NOT create the name math, so math.sqrt would still raise NameError.
Import with an alias import random as rn Call it as rn.randint(1, 6). The original name random is not created, so random.randint would raise NameError.
List what a module offers dir(math) Returns a list of the names inside the module. A lab tool, never something to memorise.
Read a function's help help(len) Prints the docstring. Works for built-ins, module functions and your own functions alike.
Check what a name is type(len) builtin_function_or_method for anything written in C, including math.sqrt; function for your own def AND for pure-Python module functions such as statistics.mean. So it is not a test of the three categories.
Remember
  • Built-in means ready to use with no import; module means you must import first and call it as module.function(); user-defined means you wrote it yourself with def.
  • Forgetting an import gives an ordinary NameError: name 'math' is not defined, because the module name was never created. Python 3.12 and later append the hint "Did you forget to import 'math'?", but the error class is still NameError.
  • Do not confuse that with ModuleNotFoundError: No module named 'xyz', which is what an import of a module that does not exist raises.
  • math.sqrt(144) returns 12.0, but math.floor and math.ceil return plain ints — the math module is not uniformly float, so check the one function you are using.
  • type() does not classify functions for you: statistics.mean reports function, exactly like a def you wrote, because that module is itself written in Python. Only C-level functions such as len and math.sqrt report builtin_function_or_method.
  • Your own functions can freely call built-ins and module functions from inside their bodies.

Creating a Function and the Flow of Execution

Quick answer def only creates a function object without running its body; a call then transfers control into that body and returns it to the exact point after the call, which you can observe rather than assume by numbering print statements.

The syntax has five parts and every one of them is examinable.

def function_name(parameters):
    """Optional docstring saying what it returns."""
    statements
    return value
  • def is the keyword that begins a definition.
  • The name follows the same rules as a variable name: letters, digits and underscores, not starting with a digit, and not a Python keyword.
  • The brackets are compulsory even when there are no parameters, as in def start():.
  • The colon at the end of the header is compulsory.
  • The body must be indented — four spaces is the convention — and every line of the body must be indented by the same amount.

The single most important idea in this whole chapter: the def statement does not run the body. It only creates the function and attaches the name to it. The body runs later, and only when you call the function with brackets. You can watch this happen.

print("before def")

def greet(name):
    print("inside greet, name =", name)

print("after def")
greet("Ananya")
print("after call")

Output:

before def
after def
inside greet, name = Ananya
after call

Read that output carefully. after def appears before anything from inside greet. Python walked past the whole definition without executing a single line of the body. Only the call on the next line made the body run.

Flow of execution

Flow of execution simply means the order in which statements actually run. Without functions it is top to bottom. A function call changes it in a very specific way: control jumps into the function body, runs it to the end or to a return, and then comes back to the exact point immediately after the call. The caller waits the whole time — nothing runs in parallel.

Rather than take that on trust, number the print statements and read the order off the screen.

def fees(base):
    print("  2. fees starts, base =", base)
    t = tax(base)
    print("  5. back in fees, tax =", t)
    return base + t

def tax(amount):
    print("    3. tax starts")
    print("    4. tax returns")
    return amount * 0.18

print("1. main starts")
total = fees(10000)
print("6. main got", total)

Output:

1. main starts
  2. fees starts, base = 10000
    3. tax starts
    4. tax returns
  5. back in fees, tax = 1800.0
6. main got 11800.0

The numbers came out in order 1 to 6, and the indentation of the output shows the depth. Step 5 is the proof that matters: after tax finished, control did not restart fees from the top and did not jump to the end of the program — it resumed at the very next line inside fees.

StepWhat is runningWhy control is there
1Main programProgram starts top to bottom
2feesCalled from main; main is paused
3-4taxCalled from fees; fees is paused
5feestax returned; fees resumes at the next line
6Main programfees returned; main resumes at the next line

The one ordering rule you must respect

Because def is a statement that has to execute before the name exists, calling a function above its definition fails:

print(mystery(3))

def mystery(x):
    return x * x

Output:

NameError: name 'mystery' is not defined

But a function may call another function that is defined below it, because the body is not executed at definition time — by the time the body actually runs, both def statements have finished. That is precisely why fees above was allowed to call tax:

def outer():
    return helper() * 2

def helper():
    return 21

print(outer())

Output:

42

Worked example — the docstring and return ending a function

The first string in a function body is its docstring; Python stores it and you can read it back. And return does not merely hand back a value, it ends the function on the spot.

def area_rect(l, b):
    """Return area of a rectangle with length l and breadth b."""
    return l * b

def check(n):
    if n > 0:
        return "positive"
    print("this line runs only for n <= 0")
    return "not positive"

print(area_rect(12, 5))
print(area_rect.__doc__)
print(area_rect.__name__)
print(check(5))
print(check(-5))

Output:

60
Return area of a rectangle with length l and breadth b.
area_rect
positive
this line runs only for n <= 0
not positive

For check(5) the print line never ran at all — the return inside the if ended the function immediately. For check(-5) the condition was false, so control fell through to the print and then to the second return. A function with no docstring has __doc__ equal to None.

Define a function def name(parameters): Colon is compulsory; the body below must be indented, conventionally by 4 spaces.
Call a function name(arguments) The brackets are what actually runs it. Writing name on its own only refers to the function object; printing it shows a function-object line with a memory address, not the result.
Function with no parameters def start(): Empty brackets are still required, both in the definition and in the call start().
Docstring """One line saying what it returns.""" Must be the first statement in the body. Read it back with name.__doc__, which is None if there is no docstring.
Empty body placeholder pass A def with a completely empty body gives IndentationError: expected an indented block after function definition; pass makes it legal and does nothing.
Definition-before-call rule the def must execute before the call runs Calling above the def gives NameError: name 'f' is not defined. Function A calling function B defined below A is fine.
Remember
  • def only creates the function; the body does not execute until a call with brackets is made — you can prove it by printing before and after the definition.
  • A call pauses the caller, runs the body, then resumes the caller at the exact statement after the call. Nothing runs in parallel.
  • Calling a function before its def statement has executed raises NameError, but a function may call another one defined below it, because its body runs later.
  • return ends the function immediately; statements after it on that path never run.
  • The brackets and the colon in the header are compulsory, and the whole body must be indented consistently.

Arguments, Parameters and Defaults

Quick answer Parameters are the names in the def and arguments are the values at the call; they are matched by position, or by name using keyword arguments, or filled in from default values that are created exactly once when the def line runs.

Two words that the board treats as a definition question. A parameter (also called a formal parameter) is the name written in the def header. An argument (also called an actual parameter) is the value you supply at the call. In def interest(p, r, t) the names p, r and t are parameters; in the call interest(50000, 8.5, 3) the numbers are the arguments.

Positional arguments

By default, arguments are matched to parameters strictly left to right. Get the order wrong and Python cannot help you, because nothing is illegal — you simply get a wrong answer, which is far more dangerous than an error.

def scored(got, total):
    return round(got / total * 100, 2)

print(scored(72, 80))
print(scored(80, 72))

Output:

90.0
111.11

A student scoring 72 out of 80 got 90%. Swapping the two arguments claims 111.11% and Python reports no problem at all.

Keyword arguments

Naming the arguments at the call makes the order irrelevant, and makes the call readable:

print(scored(total=80, got=72))
print(scored(72, total=80))

Output:

90.0
90.0

You may mix the two, but every positional argument must come before every keyword argument. Writing scored(got=72, 80) is a SyntaxError: positional argument follows keyword argument. Supplying the same parameter twice, as in scored(72, got=72), gives TypeError: scored() got multiple values for argument 'got'.

Supplying the wrong count is also an error, and the message names the missing parameter:

scored(72)

Output:

TypeError: scored() missing 1 required positional argument: 'total'

Default parameters

Give a parameter a value in the header and it becomes optional at the call. This is how you encode the common case — GST is usually 18%, discount is usually zero — without forcing the caller to repeat it.

def bill(amount, gst=18, discount=0):
    net = amount - discount
    return round(net + net * gst / 100, 2)

print(bill(1000))
print(bill(1000, 5))
print(bill(1000, discount=100))
print(bill(1000, 12, 200))

Output:

1180.0
1050.0
1062.0
896.0

The third call is the reason keyword arguments matter: to change discount while leaving gst alone, you must name it, since positionally the second slot belongs to gst.

All default parameters must come after all non-default ones. Python cannot allow otherwise, because it would then be unable to decide which parameter a lone positional argument belongs to:

def f(a=1, b):
    return a + b

Output:

SyntaxError: parameter without a default follows parameter with a default

This is a SyntaxError, caught when the file is compiled, so the program never runs at all — not a TypeError raised at call time. Board papers test that distinction.

Worked example — the mutable default trap

This is the subtlest thing in the chapter and it is worth the effort. A default value is evaluated once, at the instant the def line executes — not afresh on each call. For a number or a string that makes no visible difference. For a list it changes everything, because every call shares the same list object.

def add_marks(mark, sheet=[]):
    sheet.append(mark)
    return sheet

print(add_marks(78))
print(add_marks(92))
print(add_marks(65))
print(add_marks.__defaults__)

Output:

[78]
[78, 92]
[78, 92, 65]
([78, 92, 65],)

Each call was supposed to start with an empty sheet, and instead the function is quietly accumulating marks across calls. The last line shows why: __defaults__ holds the actual default object, and that one list has been growing all along. The fix is to use None as the default and build a fresh list inside the body:

def add_marks_safe(mark, sheet=None):
    if sheet is None:
        sheet = []
    sheet.append(mark)
    return sheet

print(add_marks_safe(78))
print(add_marks_safe(92))
print(add_marks_safe(65))

Output:

[78]
[92]
[65]

The same once-only rule shows up with an ordinary variable too. Changing the variable after the def does not change the default that was already captured:

step = 10

def jump(n, by=step):
    return n + by

step = 1000
print(jump(5))
print(jump(5, step))

Output:

15
1005

jump(5) still adds 10, because the default was fixed at 10 when the def ran. Only the explicit argument sees the new value of 1000.

Positional argument f(10, 20) Matched to parameters left to right. Swapping them silently produces a wrong result rather than an error.
Keyword argument f(b=20, a=10) Matched by name, so order does not matter. Needed to skip over a default you want to keep.
Default parameter def f(a, b=5): b becomes optional at the call. All defaults must come after all non-default parameters.
Mixing positional and keyword f(10, b=20) Legal. The reverse, f(a=10, 20), is SyntaxError: positional argument follows keyword argument.
Inspect the stored defaults f.__defaults__ A tuple of the default values, built once at def time. This is exactly why a list default remembers old data.
Safe mutable default def f(x, lst=None): Then make the first line of the body: if lst is None: lst = []. The standard, expected fix.
Remember
  • A parameter (formal parameter) appears in the def header; an argument (actual parameter) is the value supplied at the call.
  • Positional arguments match left to right — swapping them is not an error, it is a wrong answer, which is worse. Keyword arguments match by name and are order-free.
  • Two hard rules, both SyntaxErrors caught before the program runs: no non-default parameter after a default one, and no positional argument after a keyword one.
  • A default value is created ONCE when the def line executes, so a list or dictionary default is shared by every call and keeps its data between calls.
  • Fix a mutable default with None plus an if check inside the body — never with an empty list in the header.

Returning Value(s)

Quick answer return hands a value back to the caller and ends the function at once; a function without one returns None, and returning several values packs them into a single tuple that you unpack or index.

print and return are not alternatives and confusing them costs marks every year. print writes characters to the screen for a human to read. return hands a value back to the calling code so the program can store it, test it or calculate with it. Only return produces something the rest of the program can use.

def show_total(a, b):
    print(a + b)

def give_total(a, b):
    return a + b

x = show_total(40, 60)
y = give_total(40, 60)
print("x =", x)
print("y =", y)
print(type(x), type(y))

Output:

100
x = None
y = 100
 

Both functions computed 100. The first one showed it and threw it away, so x is None. The second one handed it back, so y is genuinely the integer 100.

That None is not harmless — the moment you try to calculate with it, the program stops:

print(show_total(40, 60) + 1)

Output:

100
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

Notice the 100 still printed first — the function ran, printed, and then returned None, and the addition is what failed. When you see NoneType in an error message, look for a function that forgot to return.

Three ways to end up with None

def a1():
    return

def a2():
    pass

def a3(n):
    if n > 0:
        return "pos"

print(a1(), a2(), a3(5), a3(-5))

Output:

None None pos None

A bare return, falling off the end of the body, and a return that sits in only one branch of an if. The third is the dangerous one, because it works for some inputs and silently returns None for the rest. Always ask what your function gives back when the condition is false.

Returning more than one value

Write several expressions after return, separated by commas. Python does not literally return three things — it packs them into one tuple and returns that.

def stats(marks):
    return max(marks), min(marks), round(sum(marks) / len(marks), 2)

result = stats([78, 92, 65, 88, 55])
print(result)
print(type(result))

high, low, avg = stats([78, 92, 65, 88, 55])
print("high =", high, "low =", low, "avg =", avg)

Output:

(92, 55, 75.6)

high = 92 low = 55 avg = 75.6

You may catch the tuple whole and index it as result[0], or unpack it into separate names as shown. If you unpack, the count of names must match the count of values exactly — and the two ways of getting it wrong give two different messages, so quote whichever one the question actually produces. Too few names:

p, q = stats([1, 2, 3])

Output:

ValueError: too many values to unpack (expected 2)

Too many names:

p, q, r, s = stats([1, 2, 3])

Output:

ValueError: not enough values to unpack (expected 4, got 3)

To return a list instead of a tuple, build a list explicitly with square brackets — or return something that is already a list, as split() does:

def split_name(full):
    return full.split()

print(split_name("Ananya Sharma Iyer"))
print(type(split_name("Ananya Sharma Iyer")))

Output:

['Ananya', 'Sharma', 'Iyer']

Worked example — return ends everything, including a loop

A return inside a loop does not just break the loop; it ends the entire function. Here is a check for the first failing subject in a marksheet, with a trace so you can see how far the loop actually got.

def first_fail(marks):
    for i, m in enumerate(marks):
        print("  checking", m)
        if m < 33:
            return i
    return -1

print(first_fail([78, 45, 20, 90]))

Output:

  checking 78
  checking 45
  checking 20
2

The 90 was never examined and the final return -1 was never reached. The function stopped the instant it found 20 and reported its index, 2. That trailing return -1 still matters though: it is what runs when no subject fails, and without it the function would return None for a fully passing marksheet — exactly the trap from earlier in this section.

Return one value return value Sends the value back to the caller AND ends the function on the spot.
Return nothing return A bare return. Identical in effect to falling off the end of the body: the call evaluates to None.
Return several values return a, b, c Python packs them into a single tuple. type() on the result reports tuple, not list.
Unpack the result x, y, z = f() Names must equal values. Too few names: ValueError: too many values to unpack (expected 2). Too many names: ValueError: not enough values to unpack (expected 4, got 3).
Keep the result as a tuple r = f() then use r[0] Index into the returned tuple instead of unpacking. Useful when you need only one of the values.
Return a list instead return [a, b, c] Square brackets force a list. Without them the commas alone always produce a tuple.
Remember
  • print displays a value to a human; return hands it back to the program. Only a returned value can be stored, compared or used in arithmetic.
  • A function with no return, with a bare return, or whose return sits in only one branch of an if, gives back None. Using it in arithmetic raises TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'.
  • return a, b, c returns ONE tuple, not three values. Check with type() — it reports tuple.
  • Unpacking needs exactly as many names as values, and the two failures give different messages: too few names gives ValueError: too many values to unpack (expected 2), while too many names gives ValueError: not enough values to unpack (expected 4, got 3).
  • return exits the whole function immediately, even from inside a loop — later iterations and later statements never run.

Scope of a Variable

Quick answer A name assigned inside a function is local to it and vanishes when the call ends, a name assigned at file level is global and readable everywhere, and assigning to a global name inside a function makes it local for the entire function — the cause of UnboundLocalError.

The scope of a variable is the region of the program in which its name is usable. Python gives you two that this syllabus cares about.

  • Local scope — a name created by an assignment inside a function, including its parameters. It exists only while that call is running.
  • Global scope — a name assigned at the top level of the file, outside every function. It exists for the whole run of the program.

A local name genuinely disappears when the function ends:

def f():
    inside = 99
    print("inside f:", inside)

f()
print(inside)

Output:

inside f: 99
NameError: name 'inside' is not defined

Reading a global is allowed; assigning is a different matter

Inside a function you may read a global variable with no ceremony at all:

school = "Kendriya Vidyalaya"

def who():
    print("reading global:", school)

who()

Output:

reading global: Kendriya Vidyalaya

But the moment you assign to a name inside a function, Python treats that name as a brand-new local variable, and the global of the same name is left completely untouched:

count = 10

def bump():
    count = 999
    print("inside bump, count =", count)

bump()
print("outside, count =", count)

Output:

inside bump, count = 999
outside, count = 10

UnboundLocalError — the error this rule creates

Here is the part that catches everybody. Python decides whether a name is local or global by scanning the entire function body before running any of it. If the name is assigned anywhere in the function, it is local everywhere in that function — including on lines above the assignment.

total = 100

def add_gst():
    total = total + 18
    return total

print(add_gst())

Output:

UnboundLocalError: cannot access local variable 'total' where it is not associated with a value

The assignment on the left makes total local. So the total on the right is the local one too — and it has no value yet. Python is not reading the global 100 at all.

To see that the whole body is scanned in advance, put the read on a line strictly before the assignment. It still fails:

price = 500

def show():
    print(price)
    price = 1

show()

Output:

UnboundLocalError: cannot access local variable 'price' where it is not associated with a value

The print comes first in the text and would have worked on its own. It is the assignment on the next line that retroactively makes price local for the whole function.

The global keyword, and why it is usually the wrong fix

Declaring global name at the top of the function tells Python that assignments to that name should affect the global variable instead of creating a local one:

counter = 0

def hit():
    global counter
    counter = counter + 1

hit()
hit()
hit()
print("counter =", counter)

Output:

counter = 3

The declaration must come before any use of that name in the function; writing global counter after a line that already reads counter is a SyntaxError: name 'counter' is used prior to global declaration.

It works, and you must know it for the exam. But reach for it last, because a function that edits globals can no longer be read on its own — to know what hit() does you must also know the state of the rest of the file, and any other function may have changed counter in between. The alternative is almost always better: take the value in as a parameter, send the answer back with return, and let the caller decide what to store.

def add_gst_clean(amount, rate=18):
    return amount + amount * rate / 100

total = 100
total = add_gst_clean(total)
print("total =", total)

Output:

total = 118.0

This version has no hidden dependencies. Give it 100 and it returns 118.0, every time, regardless of what the rest of the program is doing.

Worked example — mutating a global is not the same as rebinding it

This distinction decides whether you need global at all. Changing the contents of a global list is not an assignment to the name, so no keyword is needed. Pointing the name at a new list is an assignment, so it creates a local.

scores = [10, 20]

def push(v):
    scores.append(v)

def replace():
    scores = [0]
    print("  inside replace:", scores)

push(30)
print("scores =", scores)
replace()
print("scores after replace =", scores)

Output:

scores = [10, 20, 30]
  inside replace: [0]
scores after replace = [10, 20, 30]

push changed the global list without any global declaration, because append modifies the existing object. replace changed nothing outside itself, because = merely created a local name that died with the call.

One last hazard: built-in names live in their own outer scope, and a global of the same name hides them for the rest of the program.

marks = [1, 2, 3]
print(len(marks))
len = 5
print(len(marks))

Output:

3
TypeError: 'int' object is not callable

Never name a variable len, list, sum, max, str or type. The error appears far away from the line that caused it, which makes it painful to find. Doing the same thing inside a function is less damaging, because the shadowing name is local and dies with the call — but it still breaks the built-in for the rest of that function.

Local variable created by any assignment inside a def Includes the parameters. Exists only while the call runs; using the name outside gives NameError.
Global variable assigned at the top level of the file Readable inside any function with no keyword at all. Lives for the whole run of the program.
The global declaration global name Must come before any use of that name in the function. Lets assignments inside the function rebind the global.
The scope rule assignment anywhere in a function makes the name local everywhere in it So even a print placed before the assignment fails with UnboundLocalError: cannot access local variable.
Mutate versus rebind L.append(x) versus L = [x] append changes the global list with no global keyword needed; = creates a new local name and leaves the global alone.
Inspect local names locals() Returns a dict of the current function's locals, e.g. {'p': 3, 'q': 4, 'r': 7}. globals() does the same for module level.
Remember
  • Reading a global inside a function is fine; assigning to that name creates a separate local and leaves the global unchanged.
  • Python decides local versus global by scanning the whole function body before running it, so an assignment on a later line makes an earlier read fail with UnboundLocalError.
  • global lets you rebind a global from inside a function, but it must be written before any use of that name in the function, or you get SyntaxError: name 'x' is used prior to global declaration.
  • global is usually the wrong fix — pass the value in as a parameter and send the result back with return instead.
  • Mutating a global list or dictionary with append or item assignment needs no global keyword; rebinding the name with = does.
  • Naming a variable after a built-in, such as len = 5 at file level, breaks that built-in for the rest of the program with TypeError: 'int' object is not callable.

The formula sheet

Every formula in this chapter, in one place — screenshot it before your exam.

import math
Import a whole module
from math import sqrt, pi
Import selected names
import random as rn
Import with an alias
dir(math)
List what a module offers
help(len)
Read a function's help
type(len)
Check what a name is
def name(parameters):
Define a function
name(arguments)
Call a function
def start():
Function with no parameters
"""One line saying what it returns."""
Docstring
pass
Empty body placeholder
the def must execute before the call runs
Definition-before-call rule
f(10, 20)
Positional argument
f(b=20, a=10)
Keyword argument
def f(a, b=5):
Default parameter
f(10, b=20)
Mixing positional and keyword
f.__defaults__
Inspect the stored defaults
def f(x, lst=None):
Safe mutable default
return value
Return one value
return
Return nothing
return a, b, c
Return several values
x, y, z = f()
Unpack the result
r = f() then use r[0]
Keep the result as a tuple
return [a, b, c]
Return a list instead
created by any assignment inside a def
Local variable
assigned at the top level of the file
Global variable
global name
The global declaration
assignment anywhere in a function makes the name local everywhere in it
The scope rule
L.append(x) versus L = [x]
Mutate versus rebind
locals()
Inspect local names

Test yourself

Tap an answer to check it instantly — you'll see why it's right, and what to revise if it isn't.

0 correct · 0/12 answered
Q1

Predict the output: def add(x, lst=[]): lst.append(x) return lst print(add(1), add(2), add(3))

Q2

Predict the output: def f(a, b): c = a * b print(f(3, 4))

Q3

Predict the output: n = 5 def g(): n = n + 1 return n print(g())

Q4

Predict the output: def calc(a, b=5, c=2): a = a + b b = a + c c = a + b return a, b, c print(calc(1, 2))

Q5

Predict the output: def f(L): L.append(4) L = [9] return L m = [1, 2, 3] print(f(m), m)

Q6

Predict the output: a = 5 def f(p=a): return p a = 50 print(f(), f(a))

Q7

The program below prints five separate lines. In what order do they appear, top to bottom? def a(): print("A") b() print("C") def b(): print("B") print("start") a() print("end")

Q8

Predict the output: def s(n): for i in range(n): if i == 3: return i print(i, end=" ") return -1 print(s(6))

Q9

Predict the output: def m(): return 1, 2, 3 r = m() print(r, type(r).__name__, len(r))

Q10

Which of the following is a function defined in a module?

Q11

What happens when Python reads this line? def f(a=1, b): return a + b

Q12

A list scores = [10, 20] is defined at the top of a file. Inside a function you write scores.append(30). What is required for this to change the global list?

NCERT solutions & previous-year questions

Step-by-step model answers — tap a question to reveal the full solution.

NCERT questions 6

1 What is the difference between actual parameters (arguments) and formal parameters? Explain with a suitable example.Arguments and parameters

Formal parameters are the names written inside the brackets of the def header. They are placeholders — they have no value until the function is called, and they are local to the function.

Actual parameters, more commonly called arguments, are the real values supplied inside the brackets at the point of call. They are what gets copied into the formal parameters when the call happens.

Formal parameterActual parameter (argument)
Written in the def headerWritten at the function call
Only a name, no value of its ownAn actual value, variable or expression
Local to the functionBelongs to the calling code
def interest(p, r, t):
    return p * r * t / 100

principal = 50000
rate = 8.5
years = 3
print(interest(principal, rate, years))

Output:

12750.0

Here p, r and t are the formal parameters. In the call, principal, rate and years are the actual parameters, and their values 50000, 8.5 and 3 are matched to p, r and t in that order. Note that the names need not agree at all — matching is by position, not by name.

2 What is the utility of default parameters in a function? Write a program using a function to calculate simple interest, taking the rate of interest as 8.5% by default.Default parameters

Utility. A default parameter supplies a value to be used when the caller does not pass one. Its uses are:

  • It makes an argument optional, so short calls stay short for the common case.
  • It encodes the usual value in one place — change the default once and every call that relied on it is updated.
  • It lets you add a new parameter to an existing function without breaking calls already written elsewhere in the program.

Rule to remember: every parameter with a default must come after all parameters without one, otherwise Python raises SyntaxError: parameter without a default follows parameter with a default.

def simple_interest(p, t, r=8.5):
    return round(p * r * t / 100, 2)

print(simple_interest(50000, 3))
print(simple_interest(50000, 3, 12))
print(simple_interest(t=2, p=10000))

Output:

12750.0
18000.0
1700.0

The first call omits the rate and gets 8.5%. The second supplies 12% and overrides the default. The third uses keyword arguments, so the order of t and p does not matter, and the rate again falls back to 8.5%.

3 What is the difference between a local variable and a global variable? Give an example of each.Scope of a variable

A local variable is created by an assignment inside a function (parameters count as local too). It exists only while that call is running and cannot be used outside the function.

A global variable is assigned at the top level of the program, outside every function. It exists for the whole run and can be read inside any function.

Local variableGlobal variable
Created inside a functionCreated outside all functions
Lives only for the duration of the callLives for the whole program
Not accessible outside the functionReadable inside any function
Assigning to it is the normal caseRebinding it inside a function needs the global keyword
val = 50

def demo():
    val = 10
    print("local val =", val)

demo()
print("global val =", val)

Output:

local val = 10
global val = 50

Inside demo, the assignment created a new local val; the global remained 50. Adding the global keyword changes that:

val = 50

def demo2():
    global val
    val = 10
    print("inside demo2, val =", val)

demo2()
print("global val now =", val)

Output:

inside demo2, val = 10
global val now = 10
4 Write a user-defined function that accepts a list of numbers and returns the count of even numbers and the count of odd numbers in it.Function returning value(s)

The function needs to hand back two separate figures, so it returns both after a single return, separated by a comma. Python packs them into one tuple.

def count_even_odd(nums):
    e = o = 0
    for n in nums:
        if n % 2 == 0:
            e += 1
        else:
            o += 1
    return e, o

ev, od = count_even_odd([12, 7, 9, 40, 55, 66, 3])
print("even =", ev, "odd =", od)
print(count_even_odd([2, 4, 6]))

Output:

even = 3 odd = 4
(3, 0)

Two points the examiner looks for. First, the counters e and o must be initialised to 0 before the loop, not inside it. Second, note the difference in the two calls: unpacking into ev and od gives you two separate numbers, while catching the result whole shows what is really returned — a single tuple (3, 0). You could also index it as result[0] and result[1].

5 Predict the output of the following code and justify your answer. x = 100 def change(x): x = x + 100 print("in function:", x) return x y = change(x) print("x =", x, "y =", y)Scope of a variable and flow of execution

Output:

in function: 200
x = 100 y = 200

Justification, step by step:

  1. The global x is set to 100. The def statement only creates the function — its body does not run yet.
  2. change(x) is called with the argument 100. Inside the function, x is a parameter, which means it is a local variable. It receives the value 100.
  3. x = x + 100 assigns 200 to the local x. The global x is a completely different variable and is untouched.
  4. The function prints its local value, 200, and returns it.
  5. Control comes back to the calling line, where 200 is stored in y. The global x is still 100.

The key idea is that integers are immutable, so x = x + 100 inside the function cannot modify the caller’s value — it simply rebinds the local name to a new integer. The only way the outer world learns the new value is through the return, which is why y is 200.

6 Write a function that accepts a string and returns True if it is a palindrome, and False otherwise. Show its working on at least three inputs.Creating user defined functions

A palindrome reads the same forwards and backwards. The slice s[::-1] gives the reversed string, so the whole test is a single comparison. Converting to lower case and removing spaces first makes the check work on names and phrases too.

def is_palindrome(s):
    s = s.lower().replace(" ", "")
    return s == s[::-1]

print(is_palindrome("Malayalam"))
print(is_palindrome("Priodemy"))
print(is_palindrome("nayan"))

Output:

True
False
True

Two things worth noting for full marks. The function returns True or False rather than printing them, so the caller can use the result in an if or store it. And s == s[::-1] is already a Boolean expression — writing if s == s[::-1]: return True else: return False gives the same answer but is needlessly long, and examiners increasingly expect the direct form.

Previous-year board questions 4

Q1 Predict the output of the following Python code. (2 marks) def calc(a, b=5, c=2): a = a + b b = a + c c = a + b return a, b, c print(calc(1)) print(calc(1, 2)) print(calc(1, 2, 3)) print(calc(c=1, a=2)) 2023 (board pattern)

Output:

(6, 8, 14)
(3, 5, 8)
(3, 6, 9)
(7, 8, 15)

Working, call by call. Compute the three assignments in order, always using the values current at that moment.

CallStarting a, b, ca = a + bb = a + cc = a + bReturned
calc(1)1, 5, 21+5 = 66+2 = 86+8 = 14(6, 8, 14)
calc(1, 2)1, 2, 21+2 = 33+2 = 53+5 = 8(3, 5, 8)
calc(1, 2, 3)1, 2, 31+2 = 33+3 = 63+6 = 9(3, 6, 9)
calc(c=1, a=2)2, 5, 12+5 = 77+1 = 87+8 = 15(7, 8, 15)

Two traps. In the second call, the argument 2 goes to b (the second slot) and c still uses its default of 2 — students often give it to c instead. In the fourth call the arguments are given by keyword, so their written order is irrelevant: a is 2, c is 1, and b is skipped entirely and keeps its default of 5.

Also note that each returned value is printed with brackets and commas, because return a, b, c returns a single tuple. Writing the answer as 6 8 14 without brackets loses the mark.

Q2 The code given below is intended to compare two numbers. It has errors. Rewrite the code after removing all errors, and underline each correction made. (2 marks) def check(n=5, m): if n = m: print "equal" else return n, m print(check(10, 20)) 2024 (board pattern)

Errors present (four of them):

  1. def check(n=5, m): — a parameter without a default (m) cannot follow one with a default (n). Python reports SyntaxError: parameter without a default follows parameter with a default at line 1, before the program ever runs.
  2. if n = m:= is assignment; comparison needs ==.
  3. print "equal" — Python 2 syntax. In Python 3 print is a function and needs brackets.
  4. else — missing the colon.

Corrected code (in your answer script, underline each changed portion):

def check(n, m=5):
    if n == m:
        return "equal"
    else:
        return n, m

print(check(10, 20))

Output:

(10, 20)

How error 1 was fixed. Keep the parameter names and their order exactly as given and simply move the default from the first parameter to the last, so def check(n=5, m) becomes def check(n, m=5). That corrects the error while touching nothing else — the call check(10, 20) stays exactly as it was in the question, which is what “rewrite after removing the errors” asks for. Dropping the default altogether, as def check(n, m), also compiles and is normally accepted, but you lose the optional-argument behaviour, so it is the weaker answer.

The print "equal" was changed to return "equal" rather than print("equal") so that both branches hand a value back. A function that prints in one branch and returns in the other gives None for the printing branch, which the examiner will mark down. Correcting it only to print("equal") fixes the syntax but leaves that flaw.

If you now add a call that leaves the default in place, you can see it working:

print(check(5))

Output:

equal

Here n is 5 and m falls back to its default of 5, so the two are equal.

Q3 Write a user-defined function price_after_discount(amount, disc) that returns the price after applying a discount percentage, with the discount defaulting to 10%. The program must also keep a count of how many times the function has been called, using a global variable. Show the output for the calls given. (3 marks) price_after_discount(2000) price_after_discount(2000, 25) price_after_discount(disc=50, amount=800) 2024 (board pattern)
count = 0

def price_after_discount(amount, disc=10):
    global count
    count += 1
    return round(amount - amount * disc / 100, 2)

print(price_after_discount(2000))
print(price_after_discount(2000, 25))
print(price_after_discount(disc=50, amount=800))
print("calls made =", count)

Output:

1800.0
1500.0
400.0
calls made = 3

Marking points.

  • disc=10 in the header makes the discount optional, and it must be the last parameter because amount has no default.
  • global count is compulsory here. Without it, count += 1 is an assignment, so count would be treated as local and the line would raise UnboundLocalError: cannot access local variable 'count' where it is not associated with a value — it would not silently count wrongly.
  • The third call uses keyword arguments, so writing disc before amount is perfectly legal and both values are matched by name.

Follow-up often asked for the extra mark: why is global a poor design here? Because price_after_discount can no longer be understood on its own — its behaviour depends on, and silently alters, state elsewhere in the file. If the counter is genuinely needed, a cleaner design returns both values, return price, count, or keeps the counting in the calling code.

Q4 Consider the code below and answer the questions that follow. (2 marks) def show_total(a, b): print(a + b) x = show_total(40, 60) print(x) print(show_total(40, 60) + 1) (i) What is the value stored in x, and why? (ii) What happens at the last line? 2025 (board pattern)

Output:

100
None
100
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

(i) x holds None. The function show_total prints the sum but has no return statement, so it hands nothing back to the caller. Any function that falls off the end of its body returns None by default, and that is what gets stored in x. The 100 that appears on screen came from the print inside the function, not from the assignment.

(ii) The last line first calls the function, which prints 100 as before, and then tries to add 1 to what the call returned. Since the call returned None, Python attempts None + 1 and raises TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'. Note the order carefully: the 100 is printed before the error, because the function ran successfully and it is only the addition that failed.

Correction. Replace print with return in the function body:

def give_total(a, b):
    return a + b

y = give_total(40, 60)
print(y)
print(give_total(40, 60) + 1)

Output:

100
101

The rule to state in the answer: print displays a value to the user; return hands it back to the program. Only a returned value can be stored in a variable, compared, or used in a further calculation. Whenever you see NoneType in an error message, look for a function that forgot to return.

Part of Priodemy for School

Interactive CBSE lessons, Class 8–12 — free with every school on Priodemy EduSuite. Explore more chapters and labs on the Priodemy for School hub.

Ask AI