Skip to content
VLSI Mentor

Python Part I · Module 11 · Functions

Passing Lists and Dictionaries to Functions

Passing a number to a function is uneventful: the function gets the value and nothing the caller holds can be affected. Passing a list is different in a way that catches essentially everyone once, because the function does not receive a copy — it receives the same list. That means a function you called for one reason can quietly change data you are still using, and it also means the obvious mental fix does not work. The chapter settles both halves with one pair of programs that differ by a single line, and the rule that comes out of it is one you have already met twice in this course under different names.

Intermediate16 min readPythonPart IFundamentalsFunctionsMutability

Part I · Module 11 · Chapter 11.7 · Passing Lists and Dictionaries to Functions

1. A Number Is Uneventful

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def bump(n):
    n = n + 1
    return n


count = 10
print(bump(count))
print(count)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
11
10

The function produced 11; count is still 10. Nothing the function did to its parameter reached the caller, which is what most people expect of a function call.

2. A List Is Not

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def add_test(suite):
    suite.append("burst")


tests = ["smoke"]
add_test(tests)
print(tests)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['smoke', 'burst']

add_test returns nothing, and the caller's list changed anyway.

That is the whole surprise of this chapter, and it is not a special rule about functions. The function received the same list — not a copy of it, not a view of it, the same object the name tests refers to. suite.append(...) is Chapter 5.3's method, operating on that object, and tests is still pointing at it afterwards.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def show(items):
    print("same object:", items is OUTSIDE)


OUTSIDE = ["smoke"]
show(OUTSIDE)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
same object: True

Chapter 3.6's is confirms it directly: the parameter and the caller's name refer to one object.

3. But Rebinding Does Not Reach the Caller

Now the same shape with one line changed — assigning a whole new list instead of appending to the existing one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def replace(suite):
    suite = ["entirely", "new"]
    print("inside :", suite)


tests = ["smoke"]
replace(tests)
print("outside:", tests)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
inside : ['entirely', 'new']
outside: ['smoke']

Inside the function the name suite now refers to a new list. Outside, tests is untouched.

Put the two side by side and the model falls out:

Inside the functionReaches the caller?Why
suite.append("burst")yeschanges the object both names point at
suite = ["new"]nopoints the local name at a different object

One object, two names. A method that modifies the object is visible through both names. An assignment that re-points one name leaves the other where it was.

4. You Have Met This Twice Already

This is not a new rule. It is the third appearance of one idea.

Chapter 2.3 established it for plain assignment: errors = total copies a reference at that instant, and later changes to total do not follow.

Chapter 10.1, Section 5 established it for loop variables: test = test.upper() inside a loop leaves the list completely unchanged, because assigning to the loop variable re-points a name rather than writing into the collection.

This chapter is the same fact for parameters. A parameter is a name, bound to the argument's object at the moment of the call — exactly like a loop variable, exactly like any other assignment.

5. Dictionaries Behave the Same Way

Nothing about this is specific to lists. Any mutable object — a list, a dictionary, a set — behaves identically:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def record(results, name, status):
    results[name] = status


status = {"smoke": "PASS"}
record(status, "reset", "FAIL")
print(status)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'smoke': 'PASS', 'reset': 'FAIL'}

And the immutable types cannot be affected at all, because there is no operation that changes them. A tuple argument is safe by construction:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def add(record):
    record.append("x")


row = ("smoke", 4210)
add(row)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AttributeError: 'tuple' object has no attribute 'append'

Chapter 6.4's immutability, doing a job it was not obviously designed for: a tuple parameter is a guarantee that the function cannot alter what it was given.

Argument typeCan the function change the caller's value?
int, float, str, boolno — immutable
tupleno — immutable
list, dict, setyes

6. Why the Mutable Default Grew

Chapter 11.5 Section 6 showed a default list accumulating across calls and deferred the explanation to this chapter. Here it is:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
def add(item, bucket=[]):
    bucket.append(item)
    return bucket


print(add("a"))
print(add("b"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['a']
['a', 'b']

The default [] was evaluated once, when the def ran (Chapter 11.5's rule), producing one list object stored with the function. Every call that omits bucket binds the parameter to that same object — and .append() changes the object.

It is Section 2's behaviour exactly, with the caller's list replaced by the function's own hidden one. The two facts together are the whole explanation, and neither needs memorising once the model from Section 4 is in place.

7. Three Defences

Copy it on the way in. The function works on its own list and the caller's is untouched:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def add_test(suite):
    suite = list(suite)
    suite.append("burst")
    return suite


tests = ["smoke"]
result = add_test(tests)
print("caller :", tests)
print("result :", result)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
caller : ['smoke']
result : ['smoke', 'burst']

list(suite) is Chapter 5.8's copy. Note that the function now must return something, or its work is invisible.

Build and return a new collection. The accumulator pattern from Chapter 10.10, which never touches the input at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def without(suite, unwanted):
    kept = []
    for test in suite:
        if test != unwanted:
            kept.append(test)
    return kept


tests = ["smoke", "skip_me", "reset"]
print(without(tests, "skip_me"))
print(tests)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['smoke', 'reset']
['smoke', 'skip_me', 'reset']

Take an immutable type. If the function has no business changing its input, a tuple parameter makes that impossible — Section 5's AttributeError.

8. When Mutating Is the Right Choice

Not every mutation is a bug. It is right when changing the collection is the function's stated job:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def drain(queue):
    processed = []
    while queue:
        processed.append(queue.pop())
    return processed


jobs = ["j1", "j2", "j3"]
done = drain(jobs)
print("done :", done)
print("queue:", jobs)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
done : ['j3', 'j2', 'j1']
queue: []

drain emptying its argument is not a surprise — it is what the name promises. The problem in Section 2 was never mutation itself; it was mutation that the function's name and signature gave no warning of.

Two rules follow:

  • Say so in the name. add_test, drain, sort_in_place announce a change. count_errors, classify, summarise must not make one.
  • Do not both mutate and return the modified object. It invites new = sort_in_place(old) and leaves the caller believing old is unchanged. Mutate and return None, or copy and return the new value — not both.

Python's own library follows exactly this: list.sort() mutates and returns None, while sorted() leaves the input alone and returns a new list. Chapter 5.5 introduced both; this is the reason they are two functions.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
def f(items):
    items.append(99)

a = [1]
f(a)
print("A:", a)

# B
def g(items):
    items = [99]

b = [1]
g(b)
print("B:", b)

# C
def h(d):
    d["new"] = True

c = {"old": True}
h(c)
print("C:", c)

# D
def k(n):
    n += 1

d = 5
k(d)
print("D:", d)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: [1, 99]
B: [1]
C: {'old': True, 'new': True}
D: 5

A — a method changed the shared object.

B — an assignment re-pointed the local name only.

C — dictionaries behave exactly as lists do.

D — an integer is immutable, and n += 1 rebinds the local name, so nothing reaches the caller. D is B with a different type, not a different rule.

10. Exercises

Exercise 1 — predict the pair. For a function that does items.append("x") and one that does items = ["x"], predict what the caller sees after each. Verify both and explain the difference in two sentences without using the word "copy".

Exercise 2 — prove identity. Write a function that prints whether its parameter is the same object as a module-level list, using is. Call it and explain the result.

Exercise 3 — make it safe. Rewrite def add_test(suite): suite.append("burst") three ways — copying inside, returning a new list, and taking a tuple. Say which you would ship and why.

Exercise 4 — the dictionary case. Write a function that adds a key to a results dictionary, call it, and show the caller's dictionary changed. Then rewrite it so the caller's is untouched.

Exercise 5 — connect it back. In three sentences, explain the mutable-default trap using only Chapter 11.5's rule about when defaults are evaluated and this chapter's rule about what a parameter is bound to.

Exercise 6 — name the promise. For each function name, say whether it may mutate its argument: (a) count_failures (b) append_result (c) normalise (d) clear_queue (e) summarise.

Exercise 7 — sort two ways. Using tests = ["c", "a", "b"], show .sort() and sorted() side by side, printing the original after each. Say what each returned and why the library provides both.

Exercise 8 — find the surprise. This function is named as a pure query but is not. Identify the problem line, say what a caller would wrongly believe, and fix it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def longest_name(names):
    names.sort(key=len)
    return names[-1]

11. Summary

  • A function receives the same object, not a copy. is confirms it.
  • A method that modifies the object is visible to the caller; suite.append("burst") changes the caller's list.
  • An assignment inside the function is not visible; suite = ["new"] re-points a local name only.
  • One object, two names is the whole model, and it is the same fact as Chapter 2.3's assignment and Chapter 10.1's loop variable.
  • Dictionaries and sets behave exactly like lists. Immutable types — int, str, tuple — cannot be affected at all.
  • A tuple parameter is a guarantee that the function cannot alter its input.
  • The mutable-default trap is this rule plus Chapter 11.5's: one object, created once, shared by every call.
  • Three defences: copy on the way in, return a new collection, or take an immutable type.
  • Returning a new collection is the habit worth forming — the effect is visible at the call site and the function composes.
  • Mutating is correct when it is the function's stated job, and the name must say so.
  • Never both mutate and return the modified object. list.sort() returns None for exactly this reason; sorted() is the other half.

The next chapter is the module's most important distinction, and this chapter has already made half of its argument.

Where this fits

Part of the Python curriculum.