Functions, Parameters and Scope

Functions and Data Containers · 30 min

Data structures & algorithms · Foundations

Functions, parameters & scope

A name, a private set of variables, one value back out. This page traces nCr(5, 2) frame by frame, then shows why passing a number leaves the caller untouched and passing an array does not.

Pass a number, then pass an array
One long main · one scope
Four functions · four scopes

01 The idea

A name for a block, and a wall around its variables

You write a loop that multiplies 1 through 5 together. Then you need the same loop for 2, and again for 3. Copy it three times and you now have three places to fix when the multiplication is wrong, and three throwaway variable names to keep the answers apart. Give the loop a name once — fact — and you have one place and no extra names.

Naming a block does a second thing that matters more. It puts a wall around the variables inside it. Every call creates a fresh private workspace called a stack frame; the parameters and locals live in that frame, and they are destroyed when the call returns. So the only things that cross the wall are the arguments going in and the return value coming out. That is the whole model, and almost every question about functions is really a question about what exactly crossed.

A function is a named block with its own private variables. Arguments are copied in, one value comes back out through return, and nothing else crosses the boundary — unless the thing you copied in was a reference to an object the caller is still holding too.
Parameter vs argumentThe parameter is the name in the definition; the argument is the value at the call. In fact(k) the parameter is k; in fact(5) the argument is 5. The parameter is a brand-new local that starts out holding a copy of the argument.
ScopeThe region of code where a name is visible. A variable created inside a function is local to it: main cannot read fact's p, and fact cannot read main's. A global sits outside every function and is visible to all of them.
Return valueThe single value a call evaluates to, handed back the moment the frame is destroyed. A void function does its work and returns nothing, so there is no value to store — printing and returning are two different jobs.

02 Hand trace

nCr(5, 2), one frame at a time

Take nCr(5, 2) — how many ways to choose 2 things out of 5 — written the way the formula reads: 5! / (2! × 3!). Running the section 03 program end to end, main calls show, show calls nCr, and nCr makes three calls to the same fact function. Each box below is one live stack frame, newest at the right. Follow what crosses the boundary: a number goes in, a number comes out, and nothing else.

call mainmainframe 1 — main has no locals at all; its one line is show(5, 2)
call showmainshowframe 2 — parameters n = 5 and r = 2, bound by position
call nCrmainshownCrframe 3 — its own n = 5 and r = 2; it parks at the division
call fact(5)mainshownCrf(5)frame 4 — its own k = 5, its own p and i
pop → 120mainshownCr120frame 4 is destroyed; only the value 120 crosses back
pop → 2mainshownCr2fact(2) ran in a brand-new frame — k = 2, p back to 1
pop → 6mainshownCr6fact(3), because n − r is 3 — third fresh frame
pop → 10mainshow10120 / (2 × 6) = 10; nCr pops and show prints “5 choose 2 is 10”

Three calls to the same four lines of code, three separate frames, and not one of them could see the others. The first call finished with its p at 120; the second still started its own p at 1, because that is a different box created fresh at the call and thrown away at the return. Meanwhile nCr never learned anything about how fact works — it sent 5 and got 120. Section 04 shows the one case where something bigger than a number crosses that boundary.

03 Code

The same program, before and after the split

Here is nCr(5, 2) written twice. On the left it is one main with the loop copied out three times. On the right it is four named functions. The answer is 10 either way — the left prints the bare 10, the right prints 5 choose 2 is 10 — so read the diff for what it changes about names and scope, not about speed.

Before · one long main

main:    p = 1    for i in 1..5: p = p * i    # 5! = 120    q = 1    for i in 1..2: q = q * i    # 2! = 2    s = 1    for i in 1..3: s = s * i    # 3! = 6    print p / (q * s)           # 10

After · four named functions

function fact(k):            # returns a value    p = 1    for i in 1..k: p = p * i    return pfunction nCr(n, r):          # returns a value    return fact(n) / (fact(r) * fact(n - r))function show(n, r):         # void: prints, returns nothing    print n, "choose", r, "is", nCr(n, r)function main():    show(5, 2)                  # 5 choose 2 is 10

The loop earned a name because it appeared three times. The left version needs three throwaway names — p, q, s — purely to keep three results apart. On the right there is one fact called three times, so a mistake in the multiplication is one edit instead of three, and the extra names are gone.

k, p and i exist only inside fact. That is local scope, and it is exactly what the trace in section 02 showed: the first call ended with p at 120 and the second still began with p at 1. On the left, one i is shared by all three loops in a single scope, so moving a loop or forgetting a reset silently inherits the previous loop's leftovers.

n and r inside nCr are new locals, not show's variables. show(5, 2) copies 5 into show's n and 2 into show's r; the call nCr(n, r) on line 8 then copies those two values again into nCr's own n and r. Same spelling, four separate boxes. Arguments bind by position, not by name, so the first value always becomes n whatever it was called at the call site — and nothing nCr does to its n can reach back out.

show is void; fact and nCr return values. x = nCr(5, 2) stores 10. x = show(5, 2) stores nothing at all — None in Python, undefined in JavaScript, a compile error in Java or C. That is why nCr returns instead of printing: printing is a side effect, and something else in the program needed the number.

05 Cheat sheet

What gets asked about functions and scope

ConstructWhat actually happensThe gotcha
fact(5) — a callA frame is pushed; the parameter k is created as a local holding 5Arguments bind by position, not by name.
return pHands one value to the caller and destroys the frameCode after return in that branch never runs.
show(5, 2) — voidPrints and hands back nothingx = show(5,2) stores nothing, not 10.
Local variableCreated at the call, destroyed at the returnTwo calls to fact never share p or i.
Global variableOne box outside every function, alive for the whole runReading is free; assigning needs global in Python.
ShadowingA local of the same name hides the global inside that functionThe global is untouched and reappears after the return.
Passing a numberThe value is copied into the parameterx = x + 1 moves only the copy.
Passing an array or objectThe reference is copied — two names, one objecta[0] = 6 is visible to the caller.
Scope — where a name is visibleThe block it was declared in, plus anything nested inside. A parameter's scope is the function body, so k means nothing outside fact.
Lifetime — how long the box existsA local lives from the call to the return. A global lives for the whole program run. Two different questions, and interviewers ask both.
Cost — a call is not freeA frame, the argument copies and a jump. Copying a number is one machine word; copying a reference to a million-element array is also one machine word, which is why big data travels by reference.

06 Where & why

When to split, and what to hand back

Splitting code into functions is not automatically better. Every call costs a frame and a jump, and a badly placed boundary makes code harder to follow, not easier. The decision that actually shows up in interviews is narrower than “should I write a function”: should this function return a new value, or change the object it was handed?

Reach for a function when…

The same block appears more than onceThree copies of the factorial loop is three places to fix. One name is one place, and the second caller costs a line.
You would write a comment above it anywayIf the block needs a sentence explaining what it does, that sentence is the function name. nCr reads better than six lines of loops.
You want the variables to disappearp and i inside fact cannot collide with anything in main, so you stop inventing p, q, s to keep results apart.
The same logic runs on different inputsfact(5), fact(2), fact(3) is one function and three calls. Parameters are how one block serves every input.

Do something else when…

SituationBetter choiceWhy
You want the caller's number to changeReturn the new value and let the caller assign itReassigning a parameter only moves the copy. n = bump(n) is explicit and works in every language.
The function reorders or edits the list it was givenCopy the argument first, or return a new listAn array argument shares one object. A helper that quietly mutates its input is the classic “it worked yesterday” bug, and the return value still looks correct.
Several functions need the same valuePass it as a parameterA global makes the result depend on state you cannot see at the call site, and makes the function impossible to test on its own.
The block is two lines and used onceLeave it inlineA wrapper called once adds a frame, a jump and one more hop for the reader, and the name says nothing the code did not.
In an interview the question is almost never “what is a function”. It is “if I pass my list into your function and you sort it, is my list sorted afterwards?” Answer that one precisely and you have shown you understand parameters, copies and references all at once.

07 Interview questions

Say these out loud

Questions 7 to 9 — pass by value, the number that would not change, the list that did — are the ones that separate people who have read about parameters from people who have traced them.

What is a function, and why write one instead of more lines in main?
A function is a named block of code with its own private variables that takes arguments and hands back at most one value — one value if it returns, none if it is void. You write one when the same block appears more than once, or when the block needs a name to be readable. In the nCr program the factorial loop appeared three times, so naming it fact turned three places to fix into one.
What is the difference between a parameter and an argument?
The parameter is the name in the definition; the argument is the value at the call site. In fact(k) the parameter is k; in fact(5) the argument is 5. The parameter is a brand-new local variable created when the call starts and initialised from the argument, which is why writing to it does not touch anything outside.
What does scope mean?
Scope is the region of code where a name is visible. A variable created inside a function is local to it, so main cannot read fact's p and fact cannot read main's. Parameters share the scope of the body, which is why each call to fact gets its own k.
What actually happens in memory when a function is called?
A stack frame is pushed holding that call's parameters, its locals, and the address to resume at. The body runs inside that frame. On return the value is handed to the caller and the frame is popped, so every local in it stops existing. Three calls to fact means three frames created and destroyed one after another, never at the same time.
What does return actually do?
It evaluates one expression, hands that single value back to the caller, and ends the call immediately — the frame is destroyed on the way out. Anything written after return in the same branch never runs. A function without a return still returns, just with no value.
Void versus value-returning — what is the difference?
A value-returning function evaluates to something, so x = fact(5) stores 120. A void function does its work and hands nothing back, so x = show(5, 2) stores None in Python, undefined in JavaScript, and is a compile error in Java or C. If another part of the program needs the number, return it — printing it is a side effect, not a result.
Is Java pass by value or pass by reference?
Strictly pass by value, always. The confusion is that for an object the value being copied is the reference, so the callee ends up pointing at the same object and can change it. Reassigning the parameter, as in a = new int[3], does nothing to the caller. C is the same; C++ has genuine pass by reference with &, and Python behaves like Java — usually described as call by sharing.
I changed a number inside a function and the caller's variable did not change. Why?
Because the parameter is a separate box holding a copy of the value. x = x + 1 writes into that copy, and the copy is destroyed when the frame pops. If you want the caller's variable updated, return the new value and let the caller assign it: n = bump(n).
Then why does sorting a list inside a function change the caller's list?
Because what was copied was the reference, so both names point at one object. nums.sort() follows the arrow and rearranges that object, and the caller sees it. Writing nums = sorted(nums) inside the function instead rebinds only the local parameter, so the caller sees nothing — same variable name, opposite effect.
What is shadowing?
A local variable with the same name as a global hides the global inside that function. The global is not modified and reappears the moment the function returns. Python has a sharp edge here: assigning to a name anywhere in the body makes it local for the whole body, so reading it before the assignment raises UnboundLocalError rather than reading the global.
Why are global variables discouraged?
They make a function's result depend on state that is invisible at the call site, so the same call can give different answers at different times. They also make the function impossible to test on its own and turn one bug into a whole-program search. Pass the value as a parameter instead; if it genuinely never changes, a named constant is fine.
When would you not extract a function?
When the block is short, used once, and the name would say nothing the code does not already say. A call is not free — a frame, the argument copies and a jump — and a wrapper called once adds a layer for the reader to hop through. Extract for repetition or for a name that earns its place, not to hit a line count.

08 Practice problems

Draw the frames before you write the code

For each one, sketch a box per frame plus a box for any object on the heap, then decide which arrows point at the same box. The answer usually falls out of the drawing.

Name the repetition

Easy
Rewrite the eight-line main from section 03 so it uses one fact(k) function, and state what your function returns for k = 0.
Follow-up
The loop body runs zero times when k is 0, so the answer comes entirely from the line before the loop — get that line wrong and 0! silently returns 0 instead of 1.
Show the hint
Run the loop zero times on paper and read off what p still holds.

Will the caller change?

Easy
For a function swap(a, b) that does t = a; a = b; b = t on two numbers, say what the caller's two variables hold after the call. Then say what changes if you pass one two-element list and swap L[0] with L[1] instead.
Follow-up
The three lines inside are identical in both versions; only the kind of thing you handed in decides whether the caller sees anything.
Show the hint
Ask what got copied into the parameter — a value, or an arrow.

Return, do not print

Medium
Write nPr(n, r) returning n! / (n − r)! using your fact, then explain in one line why a version that prints the answer instead of returning it cannot be used to build nCr.
Follow-up
Printing and returning look identical when you run the program once; they come apart the moment a second function needs the number.
Show the hint
Try writing nCr(n, r) = nPr(n, r) / fact(r) on top of the printing version and see what value you are left holding.

The counter that will not count

Medium
Given count = 0 at the top level and a function whose body is count = count + 1 followed by a print, say exactly what Python does, what C does in the same situation, and then fix it so the global really increases.
Follow-up
The one line count = count + 1 is a runtime error in Python and a working increment in C, character for character identical. The difference is not the syntax, it is where each language decides the name lives.
Show the hint
In Python an assignment anywhere in the body makes that name local for the whole body, including the lines above it.

The helper that reorders your list

Medium
Write topThree(nums) returning the sum of the three largest numbers. The obvious way is to sort and add the last three — say what the caller's list looks like afterwards, and give two ways to leave it untouched.
Follow-up
The function returns the correct number either way, so a test on the return value passes. The damage is entirely in the list the caller is still holding, and only shows up in the code that runs next.
Show the hint
One fix copies before sorting; the other never sorts at all.

Three calls, three printed lines

Hard
Write a program with two globals, n = 5 and items = [1, 2]; a function f(n) that does n = n + 1 then prints n; a function g(lst) that appends 9 to its argument then prints lst; and a function h() that prints the global n. Call f(n), g(items) and h() in that order, write down all three printed lines, then state for each global whether it survived unchanged.
Follow-up
Two boxes are spelled n — the global one and f's parameter, which is a local from the instant the call starts — and the answer h() prints is decided entirely by which of the two n = n + 1 actually wrote into.
Show the hint
For each of the three calls ask the same two questions in order: which name is in scope right here, and did what crossed the boundary carry a value or an arrow?