CH 2 · FUNCTIONS
Unit 1 · Computational Thinking and Programming

Chapter 2
Functions

Giving a name to a block of code — so you can write it once, call it anywhere, and reason about it on its own.

1.1 Where we have got to

Chapter 1 gave you the materials

Types, operators, loops, strings, lists, tuples, dictionaries. Everything you need to compute something.

What you cannot do yet

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.

What this chapter adds

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.

Why this is the pivot chapter

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.

1.2 How this chapter runs

Six stops

  1. Defining and callingdef, and the flow of control
  2. Three kinds of function — built-in, module, user-defined
  3. Parameters and arguments — positional, default, keyword
  4. Returning values — and the print vs return distinction
  5. Scope — local, global, and UnboundLocalError
  6. Mutable arguments — where Chapter 1 comes back to bite

The two things that carry most marks

① print does not equal return

Half the lost marks in this chapter are a function that prints where it should return, and the resulting None.

② passing a list is not passing a copy

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.

2.1 The problem, before the solution

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))
OutputArea: 28.27 Area: 153.94 Area: 19.63
Three problems, not one

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.

2.2 The same program, with a function

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))
Same three numbers? 🤔
OutputArea: 28.27 Area: 153.94 Area: 19.63
Definition

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.

Eleven lines became six

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.

2.3 The anatomy

the definitiondef area(r):
    return 3.14159 * r * r
PartIn the example
def keywordstarts every definition
function namearea
parameter list(r) — may be empty
colonends the header, always
bodythe indented block below
the callarea(3)
Definition

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.

The brackets are not decoration

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.

2.4 Define before you call

WRONG — main.py1greet("Riya")
2
3def greet(name):
4    print("Hello,", name)
Which exception, and why? 🤔
TracebackTraceback (most recent call last): File "main.py", line 1, in <module> greet("Riya") ^^^^^ NameError: name 'greet' is not defined
Why 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.

The rule

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.

The subtle version

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.

2.5 The flow of control

main program the function body print("Before the call") greet("Riya") print("After the call") greet("Aarav") print("Done") def greet(name): print("Hello,", name) print("Welcome to Class XII") call 1 return call 2 Control always comes back to the line after the call.
A call is a detour, not a jump. The body runs, then execution resumes exactly where it left off.
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")
Seven lines. In what order? 🤔
OutputBefore the call Hello, Riya Welcome to Class XII After the call Hello, Aarav Welcome to Class XII Done

2.6 Check yourself

Question 1

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")
Are you ready for the answer? 🤔
Answer
Outputstart in a in b back in a 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.

3.1 Built-in functions

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))
Five lines. 🤔
Output8 9 2 7 3.14 6 [1, 2, 3] <class 'int'> 42 42
Definition

Built-in function — a function that is part of the Python interpreter itself. No import is needed and none is defined by you.

Method or function?

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.

3.2 Functions from a module

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))
Watch which ones give floats. 🤔
Output12.0 4 5 32.0 3.1416 9.5
Definition

Module — a file of ready-written functions and values. import makes it available; module.function() calls it.

Read the output types

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))
Output9.0 3.14

With from … import you use the name without the math. prefix.

3.3 The random module

dice.pyimport random

print(random.randint(1, 6))
print(random.random())
print(random.randrange(10, 20, 2))
print(random.choice(["red", "green", "blue"]))
What is the range of each line? 🤔
Output — one possible run6 0.5991024235343656 10 green
CallPossible 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
The one that is asked every year

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?"

Why the green box says "one possible run"

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.

3.4 The three kinds, side by side

KindWhere it comes fromNeeds import?Examples
Built-inthe interpreter itselfNolen, print, max, int, sorted
Modulea library file that ships with PythonYesmath.sqrt, random.randint
User-definedwritten by you with defNoarea(), greet()
The syllabus names all three. The rest of this chapter is entirely the third row.
Question 2

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

Are you ready for the answer? 🤔
Answer

(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.

4.1 Two words that are not synonyms

terms.pydef interest(p, r, t):
    return p * r * t / 100

print(interest(1000, 5, 2))
Output100.0
Definition

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.

How to keep them straight

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.

What the call actually does

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.

4.2 Positional arguments — the count must match

too few — main.py1def add(a, b):
2    return a + b
3
4print(add(3, 5))
5print(add(3))
Output8
…then the tracebackTraceback (most recent call last): File "main.py", line 5, in <module> print(add(3)) ^^^^^^ TypeError: add() missing 1 required positional argument: 'b'
too many — main.py1def add(a, b):
2    return a + b
3
4print(add(1, 2, 3))
TracebackTraceback (most recent call last): File "main.py", line 4, in <module> print(add(1, 2, 3)) ^^^^^^^^^^^^ TypeError: add() takes 2 positional arguments but 3 were given
Definition

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.

Note the exception name

It is TypeError, not ValueError or some “ArgumentError”. Read both messages carefully — Python names the missing parameter for you.

4.3 Default parameters

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))
Three numbers. 🤔
Output50.0 80.0 240.0
Definition

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.

Callprtresult
interest(1000)10005150.0
interest(1000, 8)10008180.0
interest(1000, 8, 3)100083240.0
Defaults fill from the right

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.

4.4 A default cannot come before a non-default

WRONG — main.py1def f(a=1, b):
2    return a + b
Syntax error or runtime error? 🤔
Traceback File "main.py", line 1 def f(a=1, b): ^ SyntaxError: parameter without a default follows parameter with a default
Why Python forbids it

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.

The rule

In the def line, all non-default parameters must come before all default ones. def f(b, a=1) is fine.

Note where it was caught

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.

4.5 Keyword arguments

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))
The amber line skips a parameter. 🤔
Output150.0 200.0 60.0
Definition

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.

Work the amber line out

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.

4.6 Two rules for mixing them

Rule 1 — positional first, always

WRONG — main.py1def interest(p, r=5, t=1):
2    return p * r * t / 100
3
4print(interest(r=10, 2000))
Traceback File "main.py", line 4 print(interest(r=10, 2000)) ^ SyntaxError: positional argument follows keyword argument

Once you start naming, you must keep naming.

Rule 2 — never fill a parameter twice

WRONG — main.py1def f(a, b):
2    return a - b
3
4print(f(10, a=3))
TracebackTraceback (most recent call last): File "main.py", line 4, in <module> print(f(10, a=3)) ^^^^^^^^^^ TypeError: f() got multiple values for argument 'a'

The 10 already went to a positionally.

Notice which error is which

Rule 1 breaks the grammarSyntaxError, 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.

4.7 Check yourself

Question 3

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))
Are you ready for the answer? 🤔
Answer
Output123 153 129 427
Callabcresult
calc(1)123123
calc(1, 5)153153
calc(1, c=9)129129
calc(c=7, a=4)427427

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.

5.1 return — sending an answer back

retvsprint.pydef square(n):
    return n * n

def shout(msg):
    print(msg.upper())

x = square(6)
y = shout("hello")

print(x)
print(y)
Three lines out. What is y? 🤔
OutputHELLO 36 None
Definition

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.

Read the order of the output

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.

5.2 print is not return

The 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)
Line 9 — what goes wrong? 🤔
Outputinside: 25 25 inside: 25
…then the tracebackTraceback (most recent call last): File "main.py", line 9, in <module> print(show(5) + 1) ~~~~~~~~^~~ TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
 printreturn
What it doesshows text on the screenhands a value back to the caller
Who receives itthe human watchingthe program
Can you store it?No — you get NoneYes — x = f()
Can you use it in a sum?NoYes
Ends the function?NoYes, immediately
The exam rule

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.

5.3 return ends the function there and then

grade.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))
How many times does the amber line run? 🤔
OutputA B this never runs for m >= 75 C
Once — for 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.

Why this pattern needs no else

Because return already exits, if … return chains do the job of elif. Both styles are accepted; this one is often clearer.

5.4 Returning more than one value

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)
What type comes back? 🤔
Output(45, 92, 69.0) <class 'tuple'> 45 92 69.0
It returns one value — a tuple

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.

Two ways to receive it

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.

5.5 Check yourself

Question 4

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))
Are you ready for the answer? 🤔
Answer
Outputpositive <class 'str'> None <class 'NoneType'>

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".

6.1 A local variable does not survive the call

main.py1def total(a, b):
2    s = a + b
3    print("inside :", s)
4    return s
5
6print(total(3, 4))
7print("outside:", s)
Line 7 — does s still exist? 🤔
Outputinside : 7 7
…then the tracebackTraceback (most recent call last): File "main.py", line 7, in <module> print("outside:", s) ^ NameError: name 's' is not defined
Definition

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.

This is a feature, not a limitation

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.

6.2 A global variable can be read anywhere

readglobal.pyrate = 5

def interest(p):
    print("reading global rate:", rate)
    return p * rate / 100

print(interest(1000))
print("rate is still", rate)
Three lines. 🤔
Outputreading global rate: 5 50.0 rate is still 5
Definition

Global variable — a name created at the top level of the program, outside every function. Its scope is the whole file, including inside functions.

Good practice, though

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 parameterinterest(1000, 5).

6.3 …but assigning to it makes it local

WRONG — main.py1rate = 5
2
3def bump():
4    rate = rate + 1
5    print(rate)
6
7bump()
It read rate fine last slide. Now? 🤔
TracebackTraceback (most recent call last): File "main.py", line 7, in <module> bump() File "main.py", line 4, in bump rate = rate + 1 ^^^^ UnboundLocalError: cannot access local variable 'rate' where it is not associated with a value
The rule that explains it

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.

Why students find this so unfair

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.

Is 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.

But the distinction is real.
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.

6.4 The global keyword

useglobal.pyrate = 5

def bump():
    global rate
    rate = rate + 1
    print("inside :", rate)

bump()
print("outside:", rate)
Did the global actually change? 🤔
Outputinside : 6 outside: 6
Definition

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.

Use it sparingly, and know why

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.

The exam version

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.

6.5 Scope drawn

BUILT-IN — print, len, max, int … GLOBAL — rate = 5 LOCAL — inside one call p = 1000 s = a + b born at the call, destroyed at the return ① Local ② Global ③ Built-in the order Python searches for a name
Python looks outwards: local first, then global, then built-in. The first match wins.
shadow.pyx = "global"

def shadow():
    x = "local"
    print("in shadow:", x)

shadow()
print("at module:", x)
Two lines. 🤔
Outputin shadow: local at module: global
Definition

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.

6.6 Check yourself

Question 5

Predict the exact output.

count = 0

def tick():
    global count
    count += 1
    return count

print(tick(), tick(), tick())
print(count)
Are you ready for the answer? 🤔
Answer
Output1 2 3 3

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.

Challenge

Delete the global count line. What happens now?

Answer

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.

7.1 Passing a number changes nothing outside

passint.pydef bump(n):
    n = n + 1
    print("inside :", n)

x = 10
bump(x)
print("outside:", x)
Is x 10 or 11? 🤔
Outputinside : 11 outside: 10
Chapter 1 §2.4, word for word

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.

7.2 Passing a list changes everything

passlist.pydef add_item(L):
    L.append(99)
    print("inside :", L)

marks = [10, 20]
add_item(marks)
print("outside:", marks)
Same shape as the last slide. Same answer? 🤔
Outputinside : [10, 20, 99] outside: [10, 20, 99]
marks (global) L (parameter) [10, 20, 99] ONE list object The call bound L to the same object — it did not copy it.
A parameter is just another label. append changes the object both labels point at.
The rule, stated for the exam

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.

7.3 Rebinding vs mutating — the deciding difference

rebind — no effect outsidedef rebind(L):
    L = [1, 2, 3]
    print("inside :", L)

a = [10, 20]
rebind(a)
print("outside:", a)
Outputinside : [1, 2, 3] outside: [10, 20]

= 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)
Outputinside : [99, 20] outside: [99, 20]

L[0] = 99 reached into the object and changed it.

The test to apply

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.

7.4 The mutable default argument

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"))
Three fresh boxes… or not? 🤔
Output['a'] ['a', 'b'] ['a', 'b', 'c']
Why the box is not empty the second time

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"))
Output['a'] ['b']
When exactly is a default evaluated?

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.

7.5 Check yourself

Question 6

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)
Are you ready for the answer? 🤔
Answer
Output10 5 [1, 2, 3] [1, 2, 3]

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.

8.1 Everything on one screen

Defining and calling

def name(params): the body is indented define before the call runs control returns to the line after the call built-in · module · user-defined

Parameters and arguments

parameter = in the def argument = in the call positional — count must match defaults go last keyword — order free positional before keyword

Returning

return sends a value back no return → None return exits immediately return a, b → a tuple print ≠ return

Scope and arguments

local dies at the return a global can be read assigning makes it local → UnboundLocalError global keyword to write it LEGB: local → global → built-in rebind vs mutate never default to []

8.2 Exam question 1 — find the output

3 marks

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)
Are you ready for the answer? 🤔
Answer
Output[6, 4, 10, 6] [3, 12, 15, 6] [3, 4, 5, 6]
itemn = 2resultn = 3result
33 % 2 ≠ 063 % 3 = 03
44 % 2 = 044 % 3 ≠ 012
55 % 2 ≠ 0105 % 3 ≠ 015
66 % 2 = 066 % 3 = 06

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.

8.3 Exam question 2 — find the output

3 marks

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)
Are you ready for the answer? 🤔
Answer
Output70 10 70

Line 1spend 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.

8.4 Exam question 3 — write the function

3 marks

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.

Note the word "returns". 🤔
Answer
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)
OutputHighest: 92 Lowest : 45 Average: 72.8 List : [45, 78, 92, 61, 88]
Where the three marks are

return, not print, inside the function.
returning three values, i.e. a tuple, and unpacking it at the call.
not sorting or mutating Lmax, min and sum only read it, so the last line proves the list survived.

Careful

round(72.8, 2) prints as 72.8, not 72.80 — Python does not pad a float with trailing zeros.

8.5 Exam question 4 — two-mark theory

2 + 2 marks
  1. Differentiate between a parameter and an argument, with an example.
  2. What is the difference between a local and a global variable? When is the global keyword needed?
Are you ready for the answer? 🤔
Answer 1

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.

Answer 2

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.

8.6 One last challenge

Challenge

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)
Three lines. Take your time. 🤔
Answer
Output([1, 2, 9], ['run']) ([1, 2, 9, 9], ['run', 'again']) [1, 2, 9, 9] 7
What to noticeWhy
data grew twicethe list was mutated through the parameter — §7.2
extra remembered 'run'the mutable default is one shared list — §7.4
the result is bracketedreturn L, extra returns a tuple — §5.4
size is 70 + 3 on the first call, then + 4 on the second — §6.4
End of Chapter 2

Next: Exception Handling

You have met four new tracebacks in this chapter alone — NameError, TypeError, SyntaxError, UnboundLocalError. Chapter 3 stops them ending the program.

Carry these three forward

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.