COMP 202 Foundations of Programming • McGill University, Montreal

Revision sheet: Functions, scope and program design (COMP 202)

This sheet is not a summary of the functions chapter of COMP 202: you already have the slides. It answers one question, what loses marks on functions, scope and program design in the midterm traces and in the autograded assignments of COMP 202 at McGill University, and which precise gesture avoids each loss.

The angle of the chapter is that a function is a contract in three clauses, what it takes, what it gives back, what it changes outside itself. The autograder never reads your screen: it imports your file, calls each function by name and compares what comes BACK. Every trap below is one clause left unwritten, and every repair is the sentence that writes it.

Mark this sheet as read or add it to your favourites: a free account, no password, keeps your read sheets and favourites from one visit to the next and tells you which chapter to tackle next. Create your space, an email is enough.

The thread of the chapter

A function is a contract in three clauses, what it takes, what it gives back and what it changes outside itself. Every mark lost on this chapter is one clause left unwritten: a print where a return was owed, an assignment that silently made a name local, a default list that remembers the previous call.

This chapter is part of COMP 202, Foundations of Programming (McGill)

The essentials

The contract in three clauses, and the frame it runs in

  • • A function is a contract in three clauses: what it TAKES (its parameters, with their types), what it GIVES BACK (the return value) and what it CHANGES outside itself (a list it mutates, a file it writes). The docstring states the three; a clause you cannot write means the function does two jobs.
  • • A call creates a frame: each parameter is bound to its argument, positionally in order then by keyword, and a missing one takes its default. Every name assigned in the body lives in that frame and dies at the return.
  • • return hands one value back AND ends the function on the spot. return a, b hands back one tuple. Falling off the end returns None, and print returns None too.
  • • Reading a name that is not in the frame looks in the global frame. Assigning a name ANYWHERE in the body makes it local EVERYWHERE in the body: Python decides this by scanning the body once, before the first line runs.
  • • A default value is evaluated once, when the def line runs: a number, a string, a tuple or None are safe; a list or a dictionary is shared by every call that omits the argument.
global framem = average([8, 6])m receives 7.0frame of average([8, 6])marks = [8, 6]weight = 1.0 (default)return 7.0callvalue backborn at the call, gone at the return
The call binds marks to [8, 6] and weight to its default in a frame of its own; return 7.0 sends one value back to m, and the frame is gone. Nothing assigned inside survives the return.

In an autograded assignment the grader never reads your screen: it imports your file, calls each function by the name of the handout and compares what comes back. A function that prints scores zero even when every number on the screen is right.

Value or effect: the two sides every call sits on

  • • A call that BUILDS returns the object and leaves its argument alone: sorted(values), values + [x], name.strip(), a comprehension.
  • • A call that MUTATES changes its argument and returns None: values.sort(), values.append(x), values.remove(x), d.update(other).
  • • Rebinding a parameter, x = x + 1 or values = values + [0], creates a new object in the frame and never reaches the caller. Mutating the object, values.append(0), is seen by everyone who holds it.
  • • Convention to keep: a function that returns something changes nothing, a function that changes something returns None. Say which one in the docstring, and give the two versions different names.

sorted and sort are the pair to remember: sorted builds, sort reorders. values = values.sort() is the line that stores None in the most papers of the term.

The rules in table form

Each row reads from left to right: the assumptions, then the result. A red cell is not an answer, it is the finding that the form settles nothing and the instruction to rewrite it. Every case is followed by a worked example.

What a call gives back, and what it does to its argument

Read a line before writing x = something(...): the middle column says what happens to the argument, the last one what lands in x. A red line is a value that does not exist: the line must be rewritten.

CallEffect on the argumentValue returned
sorted(values) none a new sorted list

Example: sorted([3, 1, 2]) gives [1, 2, 3], and the list is still [3, 1, 2].

values.sort() reordered in place None

Example: values = [3, 1, 2]; after values.sort() the list reads [1, 2, 3] and the call gave back None.

values.append(4) 4 added at the end None

Example: [1, 2, 3] becomes [1, 2, 3, 4]; x = values.append(4) leaves x as None.

values + [4] none a new list

Example: [1, 2, 3] + [4] is [1, 2, 3, 4], and [1, 2, 3] is unchanged.

name.upper() none, strings are immutable a new string

Example: 'ana'.upper() gives 'ANA' and name still reads 'ana', 3 characters, until name = name.upper().

print(total) none None

Example: x = print(7) shows 7 on the screen and stores None in x.

values = values.sort() reordered, then LOST None not a value

Example: values = [3, 1, 2]; after the line, values is None and len(values) raises TypeError.

What to do: values.sort() on a line of its own, or values = sorted(values).

return values.sort() reordered, caller's list None not a value

Example: sorted_marks([3, 1]) returns None while the list [3, 1] has quietly become [1, 3].

What to do: return sorted(values), or values.sort() then return values on the next line.

Every mutating method of a list returns None: append, extend, insert, remove, reverse, clear, sort, and dict.update. If a call changes something, do not use its result; if it returns something, do not expect it to change anything.

The mistakes that cost marks

These are the errors I correct most often in session. Each one costs marks on a paper, even when the reasoning behind it is right.

1. Printing the result instead of returning it

every test of that function on an autograded assignment, usually 0 of 10, and on paper the mark of every line that uses the result

What not to write

“def average(marks): print(sum(marks) / len(marks)), and it prints 7.0 just fine when I run it.”

What to write

“def average(marks): return sum(marks) / len(marks). The grader compares the value the call gives back; print gives back None, so the test reads None instead of 7.0.”

Why: print sends characters to a human, return hands a value to the program. The screen looks identical in both cases, which is why the error survives every manual run: the difference shows only when something USES the result, and x + 1 on a None raises TypeError.

2. Reading a global variable instead of the parameter

the whole function in the autograder, which imports your file and calls average on ITS lists: the answer is the average of your marks, or a NameError if the global does not exist there

What not to write

“marks = [8, 6, 9] at the top of the file, then def average(values): return sum(marks) / len(marks). It prints the right average, so the function works.”

What to write

“def average(values): return sum(values) / len(values). Every name used in the body is a parameter or a local: the function must give the same answer wherever it is called from.”

Why: In your file the global exists, so the function answers, and it answers the same number whatever argument it receives. Call it on two different lists: a function that ignores its parameter gives the same result twice, and that is the five second check that finds it.

3. Updating a global counter inside a function

the program stops before its first print, so every output question after it; and a global statement as the repair costs the style marks

What not to write

“count = 0 at the top, and inside register the line count = count + 1 reads the global count and adds one to it.”

What to write

“Assigning count anywhere in the body makes count local in the WHOLE body, so count + 1 reads a local that has no value yet: UnboundLocalError. Take count as a parameter and return count + 1.”

Why: Python decides which names are local by scanning the body once, before running a line of it. The rule is not WHERE the assignment is, it is whether there is one. That is why the error names a line that only reads the variable, and why the fix is a parameter, not a declaration.

4. Giving a list as a default value

the second test of the autograder onwards: the function passes when you run it once and fails in the grader, which calls it many times in ONE process

What not to write

“def register(mark, marks=[]): marks.append(mark); return marks. Called once it returns [8], so it works.”

What to write

“def register(mark, marks=None): and, as the first line of the body, if marks is None: marks = []. The default is evaluated once, when def runs; None is immutable, and the fresh list is built at every call.”

def register(mark, marks=[]):created once, when def runsregister(8)8the same listregister(6)86still the same listthe default is evaluated at the def line, not at each call
One list, created at the def line: register(8) fills it with 8, and register(6) finds that 8 still there and returns [8, 6]. The second call never saw an empty list.

Why: The default list is created when the def line runs, and every call that omits the argument receives the SAME list, with what the previous calls left in it. A test file calls the function ten times in a row: the tenth call returns ten elements.

5. Storing the result of a call that changes its argument

the whole question: the function returns None, and every later line that indexes the result raises TypeError far from the mistake

What not to write

“values = values.sort(), then return values: the function returns the sorted list.”

What to write

“values.sort() reorders the list in place and returns None. Either return sorted(values), a new list, or call values.sort() on its own line and return values on the next.”

Why: Python is consistent about it: a call that mutates returns None, precisely so that using its result is loud. Knowing which side of the table a call sits on is the single fact that prevents the assignment of a None.

6. Writing the return of the negative case inside the loop

the whole function: it answers on the first element, and it passes every test whose first element decides, which is why it survives your own checks

What not to write

“def all_even(values): for v in values: if v % 2 == 0: return True, and in the else: return False.”

What to write

“for v in values: if v % 2 != 0: return False, and return True AFTER the loop, once every element has been seen.”

Why: return ends the function on the spot, so a return in both branches of the first iteration means the loop never runs a second time. The shape to memorise: the counterexample returns early, the confirmation returns after the loop.

7. Leaving test calls and input() at the top level of the submitted file

often every function of the assignment at once: an input() at the top level blocks the grader until it times out, and a crashing print stops the import before any function is graded

What not to write

“My functions are all there; below them I kept name = input('Name? ') and print(average(marks)) so I could try them.”

What to write

“Put every trial under if __name__ == '__main__': or delete it before submitting. The autograder IMPORTS your file, and importing runs every top level line.”

Why: A def line only creates a function; a call at the top level runs at import time, in an environment where your marks list, your test file and your keyboard do not exist. The guard makes the trials run when YOU run the file and at no other time.

8. Expecting x = x + 1 inside the function to change the caller's x

1 to 2 marks on every trace question of the midterm, and the mirror error, expecting items.append(0) NOT to reach the caller, costs the same

What not to write

“x = 5, then def f(x): x = x + 1, then f(x), then print(x) prints 6, since f incremented x.”

What to write

“It prints 5. The parameter x is a name in the frame of f; x = x + 1 rebinds THAT name to 6, and the frame is destroyed at the return. To change the caller's value, return x + 1 and write x = f(x).”

def f(x): x = x + 1callerx5inside f, after x = x + 1x6a new boxstill 5 after the calldef g(items): items.append(0)calleritemsinside gitems120one list, two names
Left: x = x + 1 makes a NEW box inside f, and the caller's x still points to 5. Right: the two names point to ONE list, so the 0 appended inside g is what the caller reads.

Why: Rebinding a name never crosses a frame; mutating the object a name points to is seen by everyone who holds that object. The memory diagram shows the difference: the left side makes a new box, the right side writes into a shared one.

Which method to choose

Which shape of function, by what the assignment asks for

Read the verb of the specification, then pick the shape before writing a line

  • If compute a number, a string or a boolean and give it back → return the value and print nothing; the caller decides what to show

    Example: def average(marks): return sum(marks) / len(marks), so average([8, 6]) is 7.0

  • If display a report, a table, a message → a function that prints and returns None, calling the computing ones; it lives at the outer layer, often main()

    Example: def show(record): print(report_line(record)), where report_line RETURNS the string

  • If modify the list you are given: sort it, remove from it, add to it → work in place, return None, and say so in the docstring

    Example: def drop_negatives(marks): walks marks[:] and removes 2 values from [8, -2, 6, -1]

  • If build a new list from the one you are given → return the new list and leave the argument untouched, with a different name from the in place version

    Example: def without_negatives(marks): return [m for m in marks if m >= 0], 2 elements kept

  • If answer yes or no about ALL the elements → return False at the first counterexample, return True after the loop

    Example: all_even([2, 4, 7]) returns False at the 7, all_even([2, 4, 8]) reaches the final return True

  • If keep a value from one call to the next: a counter, a running list → no global, no default list: the value comes in as a parameter and goes out in the return

    Example: def register(mark, count): return count + 1, and the caller writes count = register(8, count)

    a parameter that may be omitted takes None as default, and the body builds the list

  • If a function needs another function to do its job: a key, a rule to apply → pass the function by its name, without brackets

    Example: sorted(names, key=len) passes len; key=len(names) passes the number 3 and raises TypeError

If no branch fits in one sentence, the specification describes two jobs: cut the function in two. A global statement is never the branch; in COMP 202 it costs style marks and makes the function untestable.

How the answer is expected to be written

A marker ticks steps. Here they are in order, with the concluding sentence expected word for word.

Tracing a function call on paper

When to use it: The midterm shows a def, a global variable and one or more calls, and asks what the program prints.

  1. 1 Before tracing, scan the body of each function for names on the left of an equals sign: those names are local for the whole body, whatever the global frame holds.
  2. 2 At each call, draw a frame: one line per parameter with the argument it receives, positionally in order or by keyword; a parameter with a default gets its default when the argument is omitted.
  3. 3 Execute the body line by line inside that frame. A read of a name absent from the frame goes to the global frame; an assignment writes in the frame, never outside it.
  4. 4 At return, write the value next to the call site, cross the frame out, and continue in the caller with that value. If no return is reached, the value is None.
  5. 5 Write the output one line per print, and stop at the first exception: nothing after it is printed, and name the exception.

Concluding sentence

“The call f(x) creates a frame where x is 5; x = x + 1 rebinds the frame's x to 6; the frame is destroyed at the return and the global x still reads 5, so the program prints 5.”

The trap: Tracing the body without first listing the local names: the UnboundLocalError of a counter updated inside the function is invisible to a line by line trace.

Marking: On a midterm trace, typically one mark for the frame drawn with its parameters, one per correct output line, and the whole question lost when a print is credited with a return value.

Writing a function the autograder will accept

When to use it: An assignment hands you a name, the parameters and a description of what the function returns.

  1. 1 Copy the header exactly as the handout gives it: same name, same parameters in the same order. The grader calls YOUR function by that name, and a renamed parameter breaks every keyword call.
  2. 2 Write the docstring in three clauses: what it takes and of what type, what it returns, what it changes outside itself, plus one example call with its value.
  3. 3 Write the body so that every path ends in a return of the promised type, with no print, no input and no global inside it.
  4. 4 Test with three asserts, an ordinary case, a boundary and a case that used to fail, under if __name__ == '__main__':, and call the function twice with the same arguments to be sure nothing is shared between calls.

Concluding sentence

“Takes a non-empty list of numbers and a weight, default 1.0; returns their weighted mean as a float; changes nothing. average([8, 6]) returns 7.0.”

The trap: Testing by printing: a test that prints reassuring messages scrolls past unread, while a failing assert names its line and stops.

Marking: An assignment is typically graded by the autograder for most of the marks and by a reader for the docstring and the style: a function that returns nothing scores zero on the first part whatever its body does.

Check before you hand in

Five minutes of checking recover more marks than one more problem started in a hurry.

The typical problem, taken apart

A trace question with a global counter and a default list

The program below is submitted as an answer to: write register(mark), which adds a mark to the list of marks seen so far, counts the calls in count and returns the list.

1. What does the program print? 2. The line count = count + 1 is deleted: what does it print now? 3. Rewrite register so that it keeps nothing between calls and uses no global statement, and give three asserts for it.

global framecount = 0register: functionframe of register(8)mark = 8 marks = [8]count: local, no value yetcount = count + 1 reads the LOCAL countUnboundLocalError, before any print
The two frames at the moment of the crash: the global count is 0 and untouched, the frame of register(8) already holds marks = [8], and its own count has no value when the line reads it.
python
count = 0

def register(mark, marks=[]):
    marks.append(mark)
    count = count + 1
    return marks

print(register(8))
print(register(6))
print(count)

Step 1

Scan the body of register for assignments: marks.append(mark) assigns nothing, count = count + 1 assigns count. So count is LOCAL to register, in the whole body, and the global count of line 1 is never touched by the function.

Why

This is the step a trace cannot skip: the decision is taken by Python before the first line of the body runs, and it is the one that decides question 1.

Step 2

Trace print(register(8)): a frame with mark = 8 and marks = the default list, empty when def ran. marks.append(8) makes it [8]. Then count + 1 reads the local count, which has no value yet: UnboundLocalError. The program stops. Answer to 1: it prints NOTHING, it raises.

Why

The error is raised on the line that reads count, and the append before it has already happened: the default list now holds 8, which is what question 2 turns on.

Step 3

Question 2, without the count line: register(8) returns [8], printed as [8]. register(6) receives the SAME default list, which still holds 8, appends 6 and returns [8, 6]. Then print(count) prints 0, since nothing ever changed the global. Output: [8], then [8, 6], then 0.

Why

The default is evaluated once, at the def line; every call that omits marks shares it. A student who expects [6] on the second line has just failed the second test of the autograder.

Step 4

Repair: def register(mark, count, marks=None): then if marks is None: marks = [], then marks.append(mark), then return marks, count + 1. The caller writes marks, count = register(8, count) and keeps both values.

Why

The counter comes in as a parameter and goes out in the return: no global statement, no state between calls. Returning two values is one tuple, unpacked at the call site.

Step 5

Three asserts: assert register(8, 0) == ([8], 1); assert register(6, 1, [8]) == ([8, 6], 2); and, right after a call register(8, 0), assert register(6, 0) == ([6], 1), which proves that the second call does not see the first.

Why

The third test is the one that would have caught the shared default: it is the case that used to fail, and it fails only when the tests run in the same process, which is how the grader runs them.

Step 6

Check: call register twice with the same arguments in the same session. Same answer both times, and a list you passed in has the length you gave it plus one.

Why

A function with hidden state answers differently the second time. Calling twice is the five second test that needs no reasoning about frames.

The conclusion, written out

“The program raises UnboundLocalError before its first print, because the assignment to count makes count local; without that line it prints [8], [8, 6] and 0, the default list being shared; the repaired register(mark, count, marks=None) returns (marks, count + 1) and passes its three asserts.”

The classic mistake on this problem: Answering [8], [6] and 2 to question 1: crediting the function with a global it never touches and with a fresh list it never builds.

Learn by heart

  • • return hands one value back AND ends the function; no return reached means None.
  • • A default is evaluated ONCE, at the def line: numbers, strings, tuples and None only; a list default is shared by every call.
  • • Reading a global works; assigning a name ANYWHERE in the body makes it local EVERYWHERE in the body.
  • • A frame is born at the call and dies at the return: no local survives it.
  • • A call that mutates returns None (sort, append, extend, remove, reverse, update); a call that builds returns the object (sorted, the plus sign, a comprehension).
  • • Rebinding a parameter never reaches the caller; mutating the object it names does.
  • • f is the function, f() is its result: key=len, never key=len().
  • • Compute in one function, print in main: only what is RETURNED can be tested or graded.

Frequently asked questions

Why does my Python function return None in COMP 202?

Because no return statement was reached. A function that prints its result, or that only returns inside one branch of an if, falls off its end and hands back None. Put a return on every path, and print in the caller instead: the autograder only sees what the function gives back, never what it shows on the screen.

What is UnboundLocalError and why does it point at the wrong line?

Python scans a function body once before running it: any name assigned anywhere in the body is local everywhere in it. A line such as count = count + 1 makes count local, so a read of count before that assignment, or on that very line, finds a local with no value yet. The line named is the one that reads. The fix is to take the value as a parameter and return the new one, not to add a global statement.

Why does my function work when I run it but fail the autograder?

Three usual causes. The body reads a global variable of your file instead of its parameter, so it gives your number whatever the grader passes. A default value is a list, shared between calls, so the second test sees what the first left behind. Or the file has input() or test prints at the top level, which run when the grader imports it. Each one is invisible in a single manual run.

What is the difference between print and return in Python?

print sends characters to the screen and gives the program nothing back; return hands a value to the caller and ends the function at once. Only a returned value can be stored in a variable, used in a calculation, tested with assert or checked by an autograder. A function that prints instead of returning looks right on the screen and scores zero in the tests.

Why should I not use a list as a default argument in Python?

Because the default is created once, when the def line runs, and every call that omits the argument receives that same list, with whatever earlier calls appended to it. Write the default as None and, as the first line of the body, create a fresh empty list when the parameter is None. Numbers, strings, tuples and None are safe defaults; lists and dictionaries are not.

Does changing a parameter inside a function change the variable outside?

Assigning to the parameter does not: x = x + 1 inside the function rebinds a name that lives in the frame of the call, and the caller's variable is untouched. Mutating the object does: items.append(0) writes into the list that both names point to, so the caller sees the new element. To change a number or a string in the caller, return the new value and assign it there.

Practise it

Corrected exercises: Functions, scope and program design, COMP 202

A method is proved on a paper, not on a sheet. The set for the same chapter takes each of these traps into a problem, with the solution written out step by step.

  • 10 corrected exercises
  • 100 points
  • 150 minutes
Do the exercises
Previous sheet Strings and text processing Next sheet Lists, dictionaries and the structure to choose

© Ahmed Squalli Houssaini. Revision sheet published at www.letuteurscientifique.ca/en/fiches/comp202-functions-scope-design. Free for personal and classroom use; republishing it elsewhere requires written permission (legal notice).

See also

Stuck in COMP 202?

I tutor COMP 202 at McGill in English or in French, in person in Montreal or online: get in touch for a first session.

Site by Studio Squalli