Embercode
✕
References and MemoryLesson 3 of 15

45 min read

Passing objects to functions in Python

Why it matters

"Does Python pass arguments by value or by reference?" appears in many Python interviews, and many candidates answer it wrongly. The correct answer is short, but you only trust it once you can predict the output of a program that uses it. Linked-list and tree problems depend on this rule, so it matters for the algorithm lessons that come later in the course.

By the end of this lesson you will be able to say exactly what a function can change in its caller's names, write a function that adds a node to the front of a linked list, and explain why the function must return the new front.

The first principle: a function gets a copy of the reference

When you call a function, you pass objects. Each argument is an object, and the function's parameter becomes a new name that points at that same object. Python does not copy the object. It copies the reference, which is the way a name finds its object. The function's name and the caller's name are two sticky notes on one object.

That is the whole rule. The rest of this lesson is about what it means to change "the same object" and what it means to change "the sticky note".

Computer scientists call this call by object reference, or call by sharing. The reference is passed by value, which means a copy of it is made. The name for the copy is the parameter. If you know Java, this is the same rule that Java uses for objects. The difference is that in Python every argument is an object, so the same rule applies to numbers and strings too.

Two questions to ask before you read a function

When you meet an unfamiliar function call, two questions tell you almost everything about what it can change. Ask them in this order.

Question 1: is the object mutable or immutable? A mutable object can be changed in place, such as a list, a dictionary or an object of your own class. An immutable object cannot, such as an integer, a float, a string or a tuple. An immutable object cannot be changed by the function at all, so the function can only point its name somewhere else.

Question 2: does the function change the shared object, or only its own name? A change such as items.append(x), items[0] = 5 or student.name = "Ravi" changes the shared object, and the caller sees it. An assignment to the parameter name itself, such as items = [7, 7], changes only the function's sticky note, and the caller never sees it.

Apply the two questions to a call such as process(student, scores), where student is an object and scores is a list. Find each place where the function writes. If it writes student.name = ... or scores[i] = ..., the caller sees the change. If it writes student = ... or scores = ..., the caller does not. This small habit prevents most of the mistakes described in this lesson, and it is the reasoning interviewers want to hear.

The rule in one picture

Suppose you give a friend a photocopy of your home address. Your friend can walk to your house and repaint the front door. You will see the new paint, because it is the same house. But if your friend crosses out the address on the photocopy and writes a different address, your house has not moved. Your original address slip is unchanged.

Python works the same way. The function can use its copy of the reference to reach the shared object and change it. It cannot change the caller's slip of paper, which is the caller's name.

Three cases side by side

The best way to learn the rule is to see three functions that differ in one line. Run the program, then read the explanation.

Python · editable

Type nothing into the Input box for this program.

1
[99, 2]
[99, 2]

Here is what each function does to the sticky note it was given.

change_int receives the object 1. The line n = 99 moves the parameter n to a different object, the integer 99. Integers cannot change, so there is no other way to change them. The caller's x still points at 1.

modify_list receives the list. The line a[0] = 99 follows the reference to the list and writes into it. The list is shared, so the caller sees 99 in arr[0].

reassign_list receives the list too. The line a = [7, 7] creates a brand-new list and points the function's own name at it. The caller's arr still points at the original list, which contains 99 and 2. The function moved its own sticky note, and the caller's note did not move.

If Python passed names by reference in the sense of C++ or Pascal, the third case would change arr in the caller to the new list of sevens. It does not. That is the decisive test. A function cannot make the caller's name point at a different object.

Mutable objects: change the object or move the name

Lists and objects of your own class make the two questions concrete. A list holds references in its slots, so a function can act in two different ways. It can change the object that a slot points at, which the caller sees. Or it can change the slot itself, by storing a different reference in it, which the caller also sees, because the list is shared. The only thing that stays private is the parameter name, when the function assigns a new list to it.

The next program shows all three kinds of change side by side.

Python · editable

Type nothing into the Input box for this program.

Asha K
Meena
Meena 2
Asha K

Go through the calls with the two questions. rename writes s.name, which changes the shared Student. The caller sees Asha K in the first output line, because roster[0] and asha point at the same object.

replace_first writes roster[0], which changes a slot of the shared list. The caller sees Meena in the second line, because the list is the same list.

swap_slot assigns a new list to the parameter roster. That changes only the function's own name. The caller's list still holds Meena in slot 0 and has length 2, so the third line prints Meena 2.

The last line shows that asha is still the object that rename changed. Replacing the slot in the list did not remove the old object. It only stopped the list from pointing at it.

The swap test

Almost every Python interview has a swap question. The natural attempt does not work. Here it is.

def swap(a, b):
    a, b = b, a

After x, y = 1, 2 and swap(x, y), the values of x and y are still 1 and 2. The function rebound its two names, and then discarded them when it returned. The caller never saw the swap.

Python has a neat form for swapping, a, b = b, a, but it works only on the names in one scope. The fix is to pass something that both sides share. A list is the usual choice, because the function can reach the list's items through its reference and change them.

Python · editable

Type nothing into the Input box for this program.

after swap_elements: [2, 1]
after swap: 1 2

The function swap_elements works because the two items live in the list, and the list is shared. The line arr[i], arr[j] = arr[j], arr[i] writes to two slots inside the shared object. The caller then sees the new order in pair.

There is a second correct option, which is to return the pair. Python makes that easy, because a function can return a tuple. The next section shows it.

Strings, integers and tuples

A string is immutable. Its characters never change once it is made. So a function that receives a string cannot change the caller's text. The function can only create a new string and point its own name at it. The caller keeps the old one.

def shout(s):
    s = s.upper()    # a new string is made; only the local name changes

Calling shout(t) with t equal to "hi" leaves t as "hi". The method upper() returns a new string with the text "HI", and the assignment moves the function's name to it. The caller's text is not affected.

A list is mutable, so it behaves differently. Python's standard library does not have a mutable string, but a list of pieces does the same job as Java's StringBuilder. A function that receives the list can change the text that the caller sees, because both names point at one list. The next program shows both behaviours next to each other.

Python · editable

Type nothing into the Input box for this program.

hi!
hi!
hi

add_bang changes the shared list in place, so the caller prints hi!. replace_with creates a new list and assigns it to its own name, so the caller still prints hi!. shout cannot change the string at all, so the caller prints hi.

The built-in number types int and float, and the types str, tuple and bool, are immutable too. A function cannot change the number that the caller holds. It can only point its name somewhere else. The same is true for None.

Returning results

To get a changed value back from a function, return it. That is the normal, clean way.

def double_it(n):
    return n * 2
 
x = double_it(4)     # x is 8

When a function must produce several results, return a tuple. A tuple is a fixed group of values, and the caller can unpack it into several names. The built-in divmod function is a good example, because it returns the quotient and the remainder together.

Python · editable

Type nothing into the Input box for this program.

3 2
1 9

When a function transforms a list, there are two styles. The first returns a new list and leaves the input unchanged. The second changes the input list in place and returns nothing. The built-in functions show both styles. sorted(items) returns a new list, and items.sort() sorts the list you give it and returns None. The next program does the same with both styles on a list of numbers.

Python · editable

Type nothing into the Input box for this program.

[4, 7, 10]
[5, 8, 11]
[5, 8, 11]

add_one leaves its input alone and returns a new list. The list comprehension [v + 1 for v in a] builds the new list in one line. add_one_in_place changes the input list and returns nothing. Choose the first style when the caller must keep the original data, and the second when changing the input is the point. Name the choice clearly, so the next reader does not have to guess. A common Python habit is that a function which changes its input returns None, as list.sort does.

Returning several results with names

A tuple works well for two or three values, but the caller must remember which position means what. When there are more values, or when the names matter, use a dataclass or a NamedTuple.

Python · editable

Type nothing into the Input box for this program.

3 2
DivideResult(quotient=3, remainder=2)

The dataclass decorator writes the __init__ and the printed form of the class for you. The // operator is floor division, which gives a whole number, and % gives the remainder. Two results with names are easier to read than two positions in a tuple.

Whichever you choose, the function still obeys the rule of this lesson. A returned object is a reference, so the caller and the function share it. A function that returns a stored list hands the caller access to the internal data, which is the same aliasing risk you saw in lesson 2. Return a copy when the caller must not change the original.

Linked lists and the new head

This rule matters most for linked lists. A linked list is a chain of nodes, where each node holds a value and a reference to the next node. The first node is called the head. The caller usually holds a reference to the head. The empty list has the head None.

Suppose you want to add a node at the front of the list. There are two different ideas. The first is wrong, and it is worth seeing it fail. The second is correct.

The wrong version assigns a new node to the parameter head.

def buggy_push(head, v):
    head = Node(v, head)    # only the function's name for head changes

The new node is created and linked to the old head, but the caller's head still points at the old first node. The new node is now unreachable from the caller, so the list in the caller does not change at all.

The correct version returns the new head, and the caller stores it.

def push_front(head, v):
    return Node(v, head)
 
head = push_front(head, 9)    # the caller takes the new front

The next program runs both versions and prints the list after each one. It uses a show function to print the list, which you will see again in the algorithm lessons.

Python · editable

Type nothing into the Input box for this program.

start:  2 -> 1
buggy:  2 -> 1
fixed:  9 -> 2 -> 1

Notice that Node(2, Node(1)) builds the list from the inside out. The inner node with 1 is created first, and the node with 2 points at it. The default next=None marks the end of the list.

The picture to hold in your head is this. Before the call, the caller's head points at the node holding 2. In the buggy call, the function creates a new node, links it to the node holding 2, and then drops its name for the new node when it returns. The new node is lost. In the fixed call, the function returns the new node's reference, and the caller stores it in head, so the caller now points at the node holding 9.

The same reasoning applies to a tree whose root changes, and to any structure whose first element can be replaced. Whenever an insert or delete might change the start of the structure, the function must either return the new start or be given a container that holds the start.

Worked example: an input-driven list

Here is a complete program that reads numbers and builds a list by pushing each one to the front. It uses the push_front function from above. The result shows the last number first.

class Node:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next
 
def push_front(head, v):
    return Node(v, head)
 
n = int(input())
values = input().split()
head = None
for i in range(n):
    head = push_front(head, int(values[i]))
 
parts = []
cur = head
while cur is not None:
    parts.append(str(cur.val))
    cur = cur.next
print(" -> ".join(parts))

This program reads from the terminal, so it is not in a run block. To try it, save the file and run python3 on it. When the program asks for input, type 4 on the first line and 1 2 3 4 on the second line.

4 -> 3 -> 2 -> 1

Follow the loop with the list in mind. The name head starts as None. After reading 1, the list is 1. After reading 2, the list is 2 -> 1. After reading 3 and 4, the list is 4 -> 3 -> 2 -> 1. Each step moves head in the caller to the value that push_front returns. Without that assignment, the loop would print nothing at all, because head would still be None.

Worked example: a copy of the data

Sometimes a function must not change the caller's data at all. The safe pattern is to copy the input first and change only the copy. For a list, sorted(data) returns a new sorted list and leaves data alone. Lesson 4 explains copying in depth. For now, notice how the function can promise not to change the caller's list.

def sorted_copy(values):
    return sorted(values)    # the caller's list is not touched

The caller can call sorted_copy(data) and keep data in its original order. The function name tells the reader what happens, and the reader can rely on it.

Defensive copies

A defensive copy is a copy you make to protect data from changes you do not control. It is the same idea as the copy above, applied to object attributes. If a class stores a list that the caller passed in, a later change by the caller changes the class's data too. Copying the list in the constructor prevents that.

Python · editable

Type nothing into the Input box for this program.

['Asha', 'Ravi']
['Asha']

Without the list(names), the object would hold the same reference as the caller, and the caller's append would change the team too. The rule is to copy on the way in when you store something, and copy on the way out when you return something the caller could change. If the data should never change at all, store a tuple instead of a list.

Why Python chose this rule

Passing references is simple. Every call makes a copy of its arguments' references, so a function can never reach out and rebind a name in its caller. When you read a call, you know the caller's names are safe unless the call passes an object that the function can write through. That predictability is worth a lot in a large program, where many people write functions that call each other.

The cost is that a function cannot rebind a caller's name directly. You must return the new value, or change a shared object. The linked-list example shows this cost in its clearest form, and the returned head is the answer.

Other languages make a different choice. C++ has reference parameters, written with &, that let a function change the caller's variable itself. A C++ function such as void add(int& total) can change the caller's total directly, and nothing in the call site shows that it happens. Python has no such option, so a reader can trust that a plain call such as add(total) does not rebind total. The price is that a function that needs to change a number must return it.

Searching a list without changing it

Passing the head of a list into a function is safe for reading. The function can walk the list using its own reference, and the caller's head stays where it was. Here is a function that searches the list for a value.

Python · editable

Type nothing into the Input box for this program.

True False
5

Trace the call contains(head, 3) on the list 5 -> 3 -> 8. The parameter head is a reference to the node holding 5. The name cur is a second local name, and it starts at the same node. The first pass checks 5, which is not 3, so cur moves to the next node. The second pass finds 3 and returns True.

Two things are worth noticing. The loop moves cur, not head. Moving the parameter itself would also work, but it would be confusing, because the name would no longer match what it holds. Second, the function never assigns to any attribute or to head, so the caller's list is exactly the same after the call. The last line of the program prints 5 to show that head still points at the first node.

Contrast this with the push function from earlier. Push must change the start of the list, so it returns a new head. Search only reads, so it returns a boolean. Looking at what a function returns, and whether it writes, tells you its job before you read its body.

Drawing a function call on the list

A picture makes the head problem obvious, so draw one. Start with a list of two nodes in the caller. The caller's name head points at the node holding 2. That node's next points at the node holding 1, and the last node's next is None.

Now the caller runs head = push_front(head, 9). Work through it in five moves.

Move 1. The caller evaluates the argument head, which is the reference to the node holding 2. It passes that reference into the parameter of push_front. Now two names, the caller's head and the function's head, both point at the node holding 2.

Move 2. Inside push_front, the expression Node(v, head) creates a new node. Its value is 9, and its next is a copy of the reference held by the parameter. The new node now points at the node holding 2.

Move 3. The function returns the reference to the new node. The function's frame, with its parameters, goes away.

Move 4. The caller stores the returned reference in its own head. Now the caller's name points at the node holding 9.

Move 5. Nothing else changes. The node holding 2 and the node holding 1 are still in memory, reached through the chain, so they are not garbage.

Now imagine the buggy version, push_front(head, 9) with no assignment, or a function that assigns to its parameter and returns nothing. Move 2 still creates the new node, and it still links it to the node holding 2. But move 4 never happens, so the caller's head still points at the node holding 2. The new node exists, but only the function's frame ever held its reference, and that frame is gone. The node is now unreachable from the caller, and the list the caller prints is the old one.

This is the picture to draw when a function changes a list and the caller does not see it. Find the name that should point at the new node, and check whether the caller stores the new reference in it.

Interview traps

Interviewers use a few traps on this topic. Recognise them and answer calmly.

The "Python is pass by reference" trap. If you agree with the question, the interviewer shows the reassign example and asks you to explain it. Answer with the rule first: the parameter gets a copy of the object's reference. Then show the reassign case.

The "swap two numbers" trap. The interviewer asks you to write a swap function for two integer parameters. The honest answer is that it cannot change the caller's values. Say so, show the failure in one line, and offer the list version or a returned pair.

The "linked list insert" trap. The interviewer gives you a function insert(head, v) and asks what is wrong. The answer is that the function cannot change the caller's head. Offer the fix: return the new head, or wrap the head in an object.

The "does the function change the caller's list?" trap. The interviewer shows a function that appends to a list parameter, and asks whether the caller sees the addition. Yes, it does, because a list is a mutable shared object. Then the interviewer asks what changes if the function assigns a new list to the parameter. Then it does not. Answer both halves, because the difference is the whole topic.

Common mistakes

Saying that Python passes objects by reference. The reference is passed by value, which means a copy of the reference. The difference shows up in the reassign case. If you can rebind the caller's name from inside a function, the language would be pass by reference, and Python is not.

Expecting a = [0] * 5 inside a function to resize the caller's list. The caller's name still points at the old list. Return the new list and assign it in the caller, or change the list in place with a[:] = ....

Forgetting that edits to a shared object persist. An item change made through a parameter is visible to the caller. If the function must not change the caller's data, copy the input first.

Writing a swap function on two integers. It runs, and it does nothing useful for the caller. Swap items of a list, or return a new pair.

Forgetting to store the return value. A function that returns a new head does nothing if the caller ignores the result. Python accepts push_front(head, 9) as a statement, so this bug is silent.

Using a mutable default argument. A default value in a parameter list is created once, when the function is defined, and then shared by every call. This is a common trap, and the output looks wrong in a way that is easy to miss:

Python · editable

Type nothing into the Input box for this program.

[1, 2] [1, 2]

Both calls return the same list, because the default [] is one list object that every call shares. The fix is to use None as the default and create the list inside the function:

Python · editable

Type nothing into the Input box for this program.

[1] [2]

Now each call without a list gets a new one.

Debugging walkthrough: "my list is not updated"

A student writes an insert function for a list and reports that the first insert seems to disappear.

def insert(head, v):
    head = Node(v, head)
 
head = Node(5)
insert(head, 3)
print(show(head))    # prints 5 only

Follow the steps.

Step 1: find what the function changes. It assigns to head, which is a parameter. A parameter is a name that holds a copy of the caller's reference.

Step 2: find what the caller keeps. The caller's head still points at the node with 5. The new node with 3 exists in memory, but nothing in the caller reaches it.

Step 3: decide the fix. The function must return the new front, and the caller must store it. The change is one word in the function header and one assignment in the caller.

def insert(head, v):
    return Node(v, head)
 
head = insert(head, 3)    # head now points at 3, then 5

This is a good habit. When a function might replace a reference, make it return the reference. That makes the caller's job obvious.

Variations and edge cases

None arguments. A function can receive None as a reference argument. It can then check if head is None before using it. Linked-list functions nearly always need this check, because an empty list has a None head.

Variable numbers of arguments. A function can take a variable number of arguments, written with *. Python puts them in a tuple, so the function receives one object, even when it receives no arguments. The sum function below works with zero or more numbers, and it returns 0 when no numbers are given.

Python · editable

Type nothing into the Input box for this program.

0
6
9

The * in the call total(*[4, 5]) unpacks the list into separate arguments. The function receives a tuple of the values, so values is (4, 5) inside the function.

Keyword-only parameters. A parameter written after a bare * in the definition can only be passed by name. This makes the call read clearly, and it prevents the caller from passing values in the wrong order. Python has no final keyword for parameters. The nearest tool is a tuple, which cannot be changed after it is made, or a frozen dataclass.

Changing a parameter of an immutable type. An int or a str parameter cannot change the caller's value. The same rule that applies to strings applies here.

Interview questions on this topic

"Does Python pass arguments by value or by reference?" Neither name fits exactly. The function gets a copy of the object's reference, so it can change a mutable object, but it cannot rebind the caller's name. Show the reassign case as the proof.

"Write a swap function." For two integers, it is impossible to swap the caller's values. Swap two items of a list, or return the pair as a tuple.

"Why does this insert function not update the list?" Because it changes only its own name for the head. Return the new head and let the caller store it.

"Explain argument passing in your own words." Say that Python copies each reference into the parameter. For an immutable object, the function cannot change the object, so it can only move its own name. For a mutable object, a change through the parameter is visible to the caller. Then give the reassign example: a new list assigned to the parameter does not reach the caller. Interviewers look for that example, because it separates the rule from the myth of pass by reference.

"Can a string change when you pass it to a function?" No. A string is immutable, so the function can create a new string but cannot change the existing one.

Follow-up you should expect: "How can a list be changed without returning a new head?" Put the head inside an object, as an attribute. A method that receives the object can then change its head attribute, and the caller sees the change. Many list classes work this way, so callers never need to store a returned head.

Real-world uses

Many libraries follow this rule. A function that returns a new object, such as sorted or str.strip, leaves the input unchanged. A method that changes its input, such as list.sort or list.append, changes the object the caller passed in, and its documentation says so. Reading these names carefully tells you what to expect.

In web services, a request handler often passes a request object into helper functions. A helper that stores the object in a cache creates a shared reference. Later changes to the object affect the cache too. The fix is a copy at the boundary.

Practice

Predict each answer before you run anything. Then check your answer by running the code.

  1. Predict the output of this program: def f(lines): lines.append("!"), called on lines = ["hi"], followed by print(lines).
  2. Change the function in exercise 1 so that it reads lines = ["bye"]. Does the caller see the new text? Explain in one sentence.
  3. Write def add_one(a) that returns a new list with 1 added to each element, and leaves the input unchanged.
  4. Write def append(head, v) that adds a node with value v at the end of a list, and returns the head. Handle the empty list, where head is None.
  5. Write a def clear(a) that sets every item to 0. Explain why it works, while def reset(a): a = [0] * len(a) does not.
  6. A class Team stores a list passed in by the caller. After team.add is called, the caller later adds another name to its own list and sees it appear in the team. Explain why, and name the fix.
  7. Write def is_sorted(a) that returns True when the list is in non-decreasing order. Make sure the function does not change the list.
  8. What does this print? def f(x): x = x + 1 followed by n = 5; f(n); print(n)
  9. Write a program that reads a number n and then n numbers, pushes each one to the front of a linked list, then prints the list. Use the terminal format from this lesson.
  10. Explain in two sentences why a function that returns a new head is needed, even though the function can change the nodes it is given.

Show the answer

  1. It prints ['hi', '!']. The function receives a copy of the reference to the same list. The append call changes the shared list, so the caller sees the change.

  2. No. The line lines = ["bye"] points the function's own name at a new list. The caller's name still points at the original list, which holds hi. The output is ['hi'].

  3. One possible answer:

def add_one(a):
    return [v + 1 for v in a]
  1. One possible answer:
def append(head, v):
    node = Node(v)
    if head is None:
        return node
    cur = head
    while cur.next is not None:
        cur = cur.next
    cur.next = node
    return head

The function changes the nodes it is given, by setting the last node's next. It returns the head, so the caller can store it when the list was empty.

  1. One possible answer for clear:
def clear(a):
    for i in range(len(a)):
        a[i] = 0

It works because each a[i] = 0 writes into the shared list that the caller also holds. The reset function only assigns a new list to its own name, so the caller's list is unchanged.

  1. The team stores the reference to the caller's list, not a copy, so both share one object. Adding to the caller's list changes the team's list too. The fix is to copy the list in the constructor, with list(names), so that the team owns its own data.

  2. One possible answer:

def is_sorted(a):
    for i in range(1, len(a)):
        if a[i] < a[i - 1]:
            return False
    return True

It only reads the list, so the caller's data is not changed.

  1. It prints 5. Integers cannot change, so x = x + 1 creates a new integer object and moves the function's name to it. The caller's n still points at the old object, which holds 5.

  2. One possible program, with the terminal input 3 on the first line and 7 8 9 on the second:

class Node:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next
 
def push_front(head, v):
    return Node(v, head)
 
n = int(input())
values = input().split()
head = None
for i in range(n):
    head = push_front(head, int(values[i]))
 
parts = []
cur = head
while cur is not None:
    parts.append(str(cur.val))
    cur = cur.next
print(" -> ".join(parts))

With that input, the output is 9 -> 8 -> 7.

  1. The function changes the nodes it was given, but the head itself is a reference held by the caller. Only a copy of that reference is passed in. If the new node must become the head, the function has to send the new reference back. The caller must store it, or the caller keeps pointing at the old first node.

What you now know

  • Python passes a copy of each object's reference into the parameter. The function and the caller share the object.
  • A function can change a mutable object, such as a list or a class instance, and the caller sees the change. It cannot rebind the caller's name.
  • Integers, strings and tuples are immutable. A function cannot change them for the caller, and it can only move its own name.
  • The swap of two integers in a function does nothing for the caller. Swap items of a shared list, or return a pair.
  • To change the front of a linked list, return the new head and have the caller store it.
  • Copy data when a function or a class must not share it with the caller, and avoid mutable default arguments.
Practice this topic: coding problems for References and Memory →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← None, is versus == in Python