Press m or Esc to close
Giving a name to a block of code — so you can write it once, call it anywhere, and reason about it on its own.
Types, operators, loops, strings, lists, tuples, dictionaries. Everything you need to compute something.
Every program you have written is one long straight line of statements. If you need the same calculation twice, you write it twice. If it is wrong, you fix it twice — and forget the third copy.
A way to say “this block of code has a name, takes these inputs, and gives back this answer”. After that, the rest of the year is functions calling functions.
Every remaining chapter is written as functions. File handling is a function that
reads and a function that writes. A stack is push(),
pop(), peek(). The database chapter is a function per
query. This is the last chapter that is about Python itself.
def, and the flow of controlprint vs return
distinctionUnboundLocalErrorHalf the lost marks in this chapter are a function that prints
where it should return, and the resulting
None.
The other half. Chapter 1 §2.3 said a name is a label on an object. A parameter is just another label on the same object.
repeated.pyr1 = 3 area1 = 3.14159 * r1 * r1 print("Area:", round(area1, 2)) r2 = 7 area2 = 3.14159 * r2 * r2 print("Area:", round(area2, 2)) r3 = 2.5 area3 = 3.14159 * r3 * r3 print("Area:", round(area3, 2))
① The formula is written three times.
② If 3.14159 should be math.pi, there are
three places to change and one you will miss.
③ There is no name anywhere that says “this computes an area”.
A reader has to decode the arithmetic.
area.pydef area(r): return 3.14159 * r * r print("Area:", round(area(3), 2)) print("Area:", round(area(7), 2)) print("Area:", round(area(2.5), 2))
Function — a named block of statements that performs a specific task, may accept input values, and may return a result. It is written once and executed whenever it is called.
But the real gain is not length. The formula now exists in one place, and
the word area tells the reader what is happening without their having
to read the arithmetic at all. Naming is the point of a function; reuse is a
side effect.
the definitiondef area(r): return 3.14159 * r * r
| Part | In the example |
|---|---|
def keyword | starts every definition |
| function name | area |
| parameter list | (r) — may be empty |
| colon | ends the header, always |
| body | the indented block below |
the callarea(3)
Function definition — the def statement
that creates the function. Running it does not execute the body.
Function call — writing the name followed by
( ). This is what runs the body.
area on its own is the function object.
area(3) is a call. Writing print(area) gives
you something like <function area at 0x7f…> — a classic
slip.
WRONG — main.py1greet("Riya") 2 3def greet(name): 4 print("Hello,", name)
NameError and not something about functions
Because def greet(...) is an ordinary assignment: it binds
the name greet to a function object. Until line 3 has run, the
name does not exist — exactly like using total before
total = 0.
A function must be defined before the line that calls it executes.
In practice: put all your defs at the top of the file, and the
code that uses them below.
A function may call another function defined further down, as long as the call happens after both definitions have run. It is the moment of calling that matters, not the order of the text.
flow.pydef greet(name): print("Hello,", name) print("Welcome to Class XII") print("Before the call") greet("Riya") print("After the call") greet("Aarav") print("Done")
Predict the exact output, in order.
def a(): print("in a") b() print("back in a") def b(): print("in b") print("start") a() print("end")
Note that a calls b, which is defined below it —
and that is fine, because by the time a() is actually called on the
second-to-last line, both def statements have already run.
Always available. You have used them since Chapter 1 without calling them functions.
builtins.pyprint(len("Computer")) print(max(4, 9, 2), min(4, 9, 2)) print(abs(-7), round(3.14159, 2)) print(sum([1, 2, 3]), sorted([3, 1, 2])) print(type(5), int("42"), str(42))
Built-in function — a function that is part of the
Python interpreter itself. No import is needed and none is
defined by you.
len(s) is a function — the value goes inside the brackets.
s.upper() is a method — it is called on an object with a
dot. The paper distinguishes them, so use the right word.
usemath.pyimport math print(math.sqrt(144)) print(math.floor(4.7), math.ceil(4.2)) print(math.pow(2, 5)) print(round(math.pi, 4)) print(math.fabs(-9.5))
Module — a file of ready-written functions and
values. import makes it available;
module.function() calls it.
math.sqrt(144) is 12.0, not 12 — every
math function returns a float. But
math.floor and math.ceil return ints. That
mixture is examined.
the other import formfrom math import sqrt, pi print(sqrt(81)) print(round(pi, 2))
With from … import you use the name
without the math. prefix.
random moduledice.pyimport random print(random.randint(1, 6)) print(random.random()) print(random.randrange(10, 20, 2)) print(random.choice(["red", "green", "blue"]))
| Call | Possible values |
|---|---|
randint(1, 6) | 1, 2, 3, 4, 5, 6 — both ends included |
random() | a float, 0.0 ≤ x < 1.0 |
randrange(10, 20, 2) | 10, 12, 14, 16, 18 — never 20 |
choice(seq) | any one item of the sequence |
randint(a, b) includes b. randrange(a, b)
excludes b, exactly like range() in Chapter 1. Questions
are set on precisely this difference — "which of these values can never be
generated?"
It is the only output in this course that is not reproducible. Run it again and you will get different numbers — that is the whole point of the module.
| Kind | Where it comes from | Needs import? | Examples |
|---|---|---|---|
| Built-in | the interpreter itself | No | len, print, max, int, sorted |
| Module | a library file that ships with Python | Yes | math.sqrt, random.randint |
| User-defined | written by you with def | No | area(), greet() |
Which of these can never be printed by
print(random.randint(2, 8) + random.randrange(1, 4))?
(a) 3 (b) 5 (c) 11
(d) 12
(d) 12.
randint(2, 8) gives 2 … 8 inclusive, so its largest is
8. randrange(1, 4) gives 1, 2 or 3 — never 4 — so its
largest is 3. The maximum possible total is 8 + 3 = 11, and the
minimum is 2 + 1 = 3. So 3, 5 and 11 are all reachable; 12 is not.
terms.pydef interest(p, r, t): return p * r * t / 100 print(interest(1000, 5, 2))
Parameter — a name in the function
definition: p, r, t. It is a
placeholder.
Argument — a value supplied in the function
call: 1000, 5, 2. It is the
actual data.
Parameter — in the Prototype (the def line).
Argument — in the Actual call.
Older books call them formal and actual parameters; both
vocabularies appear in question papers.
Calling interest(1000, 5, 2) performs three ordinary assignments
inside a fresh workspace — p = 1000, r = 5,
t = 2 — and then runs the body. That is all a parameter is:
a name that gets bound at call time.
too few — main.py1def add(a, b): 2 return a + b 3 4print(add(3, 5)) 5print(add(3))
too many — main.py1def add(a, b): 2 return a + b 3 4print(add(1, 2, 3))
Positional argument — matched to a parameter by its position in the call. The first argument goes to the first parameter, and so on. The counts must match exactly.
It is TypeError, not ValueError or some “ArgumentError”.
Read both messages carefully — Python names the missing parameter for you.
defaults.pydef interest(p, r=5, t=1): return p * r * t / 100 print(interest(1000)) print(interest(1000, 8)) print(interest(1000, 8, 3))
Default parameter — a parameter given a value in the
def line. If the caller omits that argument, the default is used;
if the caller supplies one, it overrides the default.
| Call | p | r | t | result |
|---|---|---|---|---|
| interest(1000) | 1000 | 5 | 1 | 50.0 |
| interest(1000, 8) | 1000 | 8 | 1 | 80.0 |
| interest(1000, 8, 3) | 1000 | 8 | 3 | 240.0 |
You cannot skip r and supply only t positionally —
interest(1000, 3) sets r to 3, not t. To skip one you need a
keyword argument, which is the next slide.
WRONG — main.py1def f(a=1, b): 2 return a + b
Ask what f(7) would mean. Is 7 the a that already has
a default, or the b that must be supplied? There is no answer —
so Python refuses the definition, not the call.
In the def line, all non-default parameters must come before
all default ones. def f(b, a=1) is fine.
This is a SyntaxError — the file never runs, and Chapter 3 told you that
no try…except can catch it. It is caught the moment Python reads
the def line, before anything is called.
keyword.pydef interest(p, r=5, t=1): return p * r * t / 100 print(interest(1000, t=3)) print(interest(r=10, p=2000)) print(interest(t=2, r=6, p=500))
Keyword argument — an argument written as
name=value in the call. It is matched to the parameter by
name, so the order does not matter and parameters in the middle
may be skipped.
interest(1000, t=3) → p=1000 positionally,
t=3 by name, and r falls back to its default
5. So 1000 × 5 × 3 / 100 = 150.0.
WRONG — main.py1def interest(p, r=5, t=1): 2 return p * r * t / 100 3 4print(interest(r=10, 2000))
Once you start naming, you must keep naming.
WRONG — main.py1def f(a, b): 2 return a - b 3 4print(f(10, a=3))
The 10 already went to a
positionally.
Rule 1 breaks the grammar → SyntaxError, caught before running.
Rule 2 is grammatical but impossible → TypeError, caught at the
moment of the call. That is exactly the Chapter 3 distinction, seen again.
Predict the exact output — four lines.
def calc(a, b=2, c=3): return a * 100 + b * 10 + c print(calc(1)) print(calc(1, 5)) print(calc(1, c=9)) print(calc(c=7, a=4))
| Call | a | b | c | result |
|---|---|---|---|---|
| calc(1) | 1 | 2 | 3 | 123 |
| calc(1, 5) | 1 | 5 | 3 | 153 |
| calc(1, c=9) | 1 | 2 | 9 | 129 |
| calc(c=7, a=4) | 4 | 2 | 7 | 427 |
The digits of the answer are a, b, c — which is why this shape of question is so popular. Read the answer and you can see which parameter got which value.
return — sending an answer backretvsprint.pydef square(n): return n * n def shout(msg): print(msg.upper()) x = square(6) y = shout("hello") print(x) print(y)
y? 🤔return — ends the function immediately and sends a
value back to the caller, where it becomes the value of the call expression.
A function with no return statement returns
None.
HELLO printed first — because shout("hello") ran
before either print(x) or print(y). The function did its
printing during the call; it just had nothing to give back afterwards.
print is not returnThe single most expensive confusion in this chapter.
main.py1def show(n): 2 print("inside:", n * n) 3 4def give(n): 5 return n * n 6 7show(5) 8print(give(5)) 9print(show(5) + 1)
print | return | |
|---|---|---|
| What it does | shows text on the screen | hands a value back to the caller |
| Who receives it | the human watching | the program |
| Can you store it? | No — you get None | Yes — x = f() |
| Can you use it in a sum? | No | Yes |
| Ends the function? | No | Yes, immediately |
If a question says “write a function that returns…”, a print in
place of return loses the mark even though the screen looks right.
Read the verb in the question.
return ends the function there and thengrade.pydef grade(m): if m >= 90: return "A" if m >= 75: return "B" print("this never runs for m >= 75") return "C" print(grade(95)) print(grade(80)) print(grade(60))
grade(60) only
For 95 and 80 the function left at a return, so nothing
below it ran. Only 60 fell through both ifs and reached the
print.
else
Because return already exits, if … return chains do
the job of elif. Both styles are accepted; this one is often
clearer.
stats.pydef stats(L): return min(L), max(L), sum(L) / len(L) result = stats([45, 78, 92, 61]) print(result) print(type(result)) lo, hi, avg = stats([45, 78, 92, 61]) print(lo, hi, avg)
Chapter 1 §7.1 said the brackets are optional and the comma makes the
tuple. So return a, b, c packs three values into one tuple, and
lo, hi, avg = stats(...) unpacks it again.
Catch it as one name and you get a tuple you can index —
result[0]. Catch it in three names and you get three separate
values. Same function, both legal.
What is printed, and what is the type of r in each case?
def f(n): if n > 0: return "positive" r1 = f(5) r2 = f(-5) print(r1, type(r1)) print(r2, type(r2))
For n = -5 the if is False, so the function reaches its
end without executing any return — and every function that falls
off the end returns None. A function is allowed to return
different types on different calls, and that is usually a design fault worth
fixing: give it an else: return "not positive".
main.py1def total(a, b): 2 s = a + b 3 print("inside :", s) 4 return s 5 6print(total(3, 4)) 7print("outside:", s)
s still exist? 🤔Local variable — a name created inside a function. It exists only while that call is running and is destroyed when the function returns. Its scope is the function body.
It is why you can use i as a loop counter in fifty different functions
without them interfering. Each call gets a fresh, private workspace.
readglobal.pyrate = 5 def interest(p): print("reading global rate:", rate) return p * rate / 100 print(interest(1000)) print("rate is still", rate)
Global variable — a name created at the top level of the program, outside every function. Its scope is the whole file, including inside functions.
A function that reads globals is harder to reuse and harder to test, because
its answer depends on something invisible in its call. Prefer passing the
value in as a parameter — interest(1000, 5).
WRONG — main.py1rate = 5 2 3def bump(): 4 rate = rate + 1 5 print(rate) 6 7bump()
rate fine last slide. Now? 🤔Python scans the whole function body before running it. If a name is assigned to anywhere in the body, that name is local for the entire function — including on lines above the assignment.
So on line 4 rate is a local variable, and the right-hand side
tries to read it before it has ever been given a value.
Reading a global works. Reading-then-assigning fails. The difference is not where you read it but whether the name is assigned anywhere in that function.
UnboundLocalError just a NameError?
It is a subclass of NameError — so
except NameError: will catch it, and calling it "a kind of NameError"
in a written answer is correct.
NameError — Python could not find the name anywhere.UnboundLocalError — Python knows the name is local to this function,
but it has not been assigned yet.
Many question banks and older NCERT-aligned material simply write
NameError here, because Python 2 and early Python 3 reported it that
way. Write UnboundLocalError if you are asked to name what
modern Python raises, and add "a subclass of NameError" — that answer cannot be
marked wrong either way.
global keyworduseglobal.pyrate = 5 def bump(): global rate rate = rate + 1 print("inside :", rate) bump() print("outside:", rate)
global — a declaration inside a function stating that the named variable refers to the global one, so assignments inside the function change the global rather than creating a local.
A function that changes a global has an invisible effect: reading the
call bump() tells you nothing about what it altered. Prefer
rate = bump(rate) — the change is then written where anyone can
see it.
You will be asked "what is the output" for code with and without the
global line. The two answers differ on the last line only —
inside the function it looks the same either way.
shadow.pyx = "global" def shadow(): x = "local" print("in shadow:", x) shadow() print("at module:", x)
Shadowing — a local name hiding a global one of the same name, for the duration of the call. The global is untouched; it simply cannot be seen from inside.
Predict the exact output.
count = 0 def tick(): global count count += 1 return count print(tick(), tick(), tick()) print(count)
All three calls run before the first print shows anything —
Python evaluates every argument first, left to right. Each call increments the
same global, so they give 1, 2 and 3, and count is left at
3.
Delete the global count line. What happens now?
UnboundLocalError, on the very first call. Without the declaration,
count += 1 makes count local for the whole function —
and += has to read it before it can write it. Nothing is
printed at all.
passint.pydef bump(n): n = n + 1 print("inside :", n) x = 10 bump(x) print("outside:", x)
x 10 or 11? 🤔n + 1 built a new int object and rebound the local name
n to it. The global name x never moved. An int is
immutable, so there was nothing to change in place.
passlist.pydef add_item(L): L.append(99) print("inside :", L) marks = [10, 20] add_item(marks) print("outside:", marks)
append changes the object both labels point at.Arguments are passed by reference to the object. If the object is mutable and the function mutates it, the caller sees the change. If the object is immutable, or the function only rebinds its parameter, the caller sees nothing.
rebind — no effect outsidedef rebind(L): L = [1, 2, 3] print("inside :", L) a = [10, 20] rebind(a) print("outside:", a)
= pointed the local label at a new list.
The caller's list was never touched.
mutate — visible outsidedef mutate(L): L[0] = 99 print("inside :", L) b = [10, 20] mutate(b) print("outside:", b)
L[0] = 99 reached into the object and
changed it.
Look at what is immediately left of the =.
A bare name (L = …) → rebinding, invisible outside.
Anything with brackets or a dot (L[0] = …,
L.append(…)) → mutating, visible outside.
The most surprising output in the whole syllabus.
surprise.pydef collect(item, box=[]): box.append(item) return box print(collect("a")) print(collect("b")) print(collect("c"))
The default value [] is evaluated once, when the
def line runs — not on every call. So all three calls share
the same list object, and each append adds to it.
FIXEDdef collect(item, box=None): if box is None: box = [] box.append(item) return box print(collect("a")) print(collect("b"))
Once, at the moment the def statement executes — which is when
Python first reads the file, not when the function is called. The resulting object
is stored with the function and reused for every call that omits that argument.
def f(x=[]): → one list, shared by all calls, and it
remembers.def f(x=0): → one int, shared by all calls, but an int is
immutable, so no call can change it and nothing surprising happens.
That is why the trap needs a mutable default to appear at all. The safe rule
is absolute: never use a list, dictionary or set as a default value. Use
None and build the real default inside the body.
Note: box is None, not box == None —
Chapter 1 §2.4 said is is reserved for exactly this.
Predict the exact output — four lines.
n = 5 def change(n): n = n * 2 return n print(change(n)) print(n) L = [1, 2] def grow(L): L.append(3) return L print(grow(L)) print(L)
Line 2 is 5, line 4 is changed. Both functions were given a global, both
returned something, both used the same parameter name as the global. The only
difference is that change rebound an immutable int, while
grow mutated a list. That single distinction is the answer to
every question of this shape.
Write the output of the following code.
def process(L, n=2): out = [] for item in L: if item % n == 0: out.append(item) else: out.append(item * n) return out nums = [3, 4, 5, 6] print(process(nums)) print(process(nums, 3)) print(nums)
| item | n = 2 | result | n = 3 | result |
|---|---|---|---|---|
| 3 | 3 % 2 ≠ 0 | 6 | 3 % 3 = 0 | 3 |
| 4 | 4 % 2 = 0 | 4 | 4 % 3 ≠ 0 | 12 |
| 5 | 5 % 2 ≠ 0 | 10 | 5 % 3 ≠ 0 | 15 |
| 6 | 6 % 2 = 0 | 6 | 6 % 3 = 0 | 6 |
The third line is the mark most often lost. nums is unchanged,
because the function built a new list called out and never
mutated L.
Write the output. Note the global statement carefully.
total = 100 def spend(amount): global total total = total - amount return total def peek(amount): total = total_of(amount) return total def total_of(a): return a * 2 print(spend(30)) print(peek(5)) print(total)
Line 1 — spend declared global total, so it really
changed the global from 100 to 70.
Line 2 — inside peek, total is a local
variable, because it is assigned there and there is no global
declaration. It is set to total_of(5) = 10 and returned.
Line 3 — the global is still 70. peek's local
total died when the function returned and never touched it.
Write a function marks_report(L) that takes a list of marks and
returns a tuple containing the highest mark, the lowest mark and the average,
rounded to two decimal places. The original list must not be modified.
Then call it for [45, 78, 92, 61, 88] and print the three values on
separate lines.
report.pydef marks_report(L): high = max(L) low = min(L) avg = round(sum(L) / len(L), 2) return high, low, avg marks = [45, 78, 92, 61, 88] h, l, a = marks_report(marks) print("Highest:", h) print("Lowest :", l) print("Average:", a) print("List :", marks)
① return, not print, inside the
function.
② returning three values, i.e. a tuple, and unpacking it at the
call.
③ not sorting or mutating L — max,
min and sum only read it, so the last line proves
the list survived.
round(72.8, 2) prints as 72.8, not
72.80 — Python does not pad a float with trailing zeros.
global keyword needed?A parameter is the variable named in the function definition; it acts as a placeholder for the value the function will receive. An argument is the actual value supplied in the function call, which is bound to the parameter when the call is made.
In def area(r): … area(3), the name r is the
parameter and the value 3 is the argument.
A local variable is created inside a function; it exists only while that call is executing and cannot be accessed from outside. A global variable is created at the top level of the program; it can be read anywhere in the file, including inside functions.
The global keyword is needed when a function must assign to a
global variable. Without it, any assignment inside the function creates a new local
variable instead, and reading the name before that assignment raises
UnboundLocalError.
Predict the exact output. Every idea in this chapter is in here.
data = [1, 2] size = 0 def work(L, extra=[], label="run"): global size L.append(9) extra.append(label) size = size + len(L) return L, extra print(work(data)) print(work(data, label="again")) print(data, size)
| What to notice | Why |
|---|---|
data grew twice | the list was mutated through the parameter — §7.2 |
extra remembered 'run' | the mutable default is one shared list — §7.4 |
| the result is bracketed | return L, extra returns a tuple — §5.4 |
size is 7 | 0 + 3 on the first call, then + 4 on the second — §6.4 |
You have met four new tracebacks in this chapter alone —
NameError, TypeError, SyntaxError,
UnboundLocalError. Chapter 3 stops them ending the program.
① A function that crashes returns nothing — the caller never gets its
answer, so the crash has to be handled somewhere.
② The call stack — a function calling a function — is what a
traceback prints, innermost frame last.
③ raise and assert in Chapter 3 both live inside
functions, and both use the return-style “leave immediately”
behaviour you have just learned.