1.2 The Python Memory Model: Functions and Parameters

Terminology

Let’s use this simple example to review some terminology that should be familiar to you:

# Example 1.

def mess_about(n: int, s: str) -> None:
    message = s * n
    print(message)

if __name__ == '__main__':
    count = 13
    word = 'nonsense'
    mess_about(count, word)

In the function declaration, each variable in the parentheses is called a parameter. Here, n and s are parameters of function mess_about. When we call a function, each expression in the parentheses is called an argument. The arguments in our one call to mess_about are count and word.

How function calls are tracked

Python must keep track of the function that is currently running, and any variables defined inside of it. It stores this information in something called a stack frame, or just “frame” for short.

Every time we call a function, the following happens:

  1. A new frame is created and placed on top of any frames that may already exist. We call this pile of frames the call stack.
  2. Each parameter is defined inside that frame.
  3. The arguments in the function call are evaluated, in order from left to right. Each is an expression, and evaluating it yields the id of an object. Each of these ids is assigned to the corresponding parameter.

Then the body of the function is executed.

In the body of the function there may be assignment statements. We know that if the variable on the left-hand-side of the assignment doesn’t already exist, Python will create it. But with the awareness that there may be a stack of frames, we need a slightly more detailed rule:

If the variable on the left-hand-side of the assignment doesn’t already exist in the top stack frame, Python will create it in that top stack frame.

For example, if we stop our above sample code right before printing message, this is the state of memory:

variables

Notice that the top stack frame, for our call to mess_about, includes the new variable message. We say that any new variables defined inside a function are local variables; they are local to a call to that function.

When a function returns, either due to executing a return statement or getting to the end of the function, the frame for that function call is deleted. All the variables defined in it—both parameters and local variables—disappear. If we try to refer to them after the function has returned, we get an error. For example, when we are about to execute the final line in this program,

# Example 2. (Same as Example 1, but with a print statement added.)

def mess_about(n: int, s: str) -> None:
    message = s * n
    print(message)

if __name__ == '__main__':
    count = 13
    word = 'nonsense'
    mess_about(count, word)
    print(n)

this is the state of memory,

variables

which explains why the final line produces the error NameError: name 'n' is not defined.

Passing an argument creates an alias

What we often call “parameter passing” can be thought of as essentially variable assignment. In the example above, it is as if we wrote

n = count
s = word

before the body of the function.

If an argument to a function is a variable, what we assign to the function’s parameter is the id of the object that the variable references. This creates an alias. As you should expect, what the function can do with these aliases depends on whether or not the object is mutable.

Passing a reference to an immutable object

If we pass a reference to an immutable object, we can do whatever we want with the parameter and there will be no effect outside the function.

Here’s an example:

# Example 3.

def emphasize(s: str) -> None:
    s = s + s + '!'

if __name__ == '__main__':
    word = 'moo'
    emphasize(word)
    print(word)

This code prints plain old moo. The reason is that, although we set up an alias, we don’t (and can’t) change the object that both word and s reference; we make a new object. Here’s the state of memory right before the function returns:

variables

Once the function is over and the stack frame is gone, the string object we want (with moomoo!) will be inaccessible. The net effect of this function is nothing at all. It doesn’t change the object that s refers to, it doesn’t return anything, and it has no other effect such as taking user rinput or printing to the screen. The one thing it does do, making s refer to something new, doesn’t last beyond the function call.

If we want to use this function to change word, the solution is to return the new value and then, in the calling code, assign that value to word:

# Example 4.

def emphasized(s: str) -> str:
    return s + s + '!'

if __name__ == '__main__':
    word = 'moo'
    word = emphasized(word)
    print(word)

This code prints out moomoo!. Notice that we changed the function name from emphasize to emphasized. This makes sense when we consider the context of the function call:

    word = emphasized(word)

Our function call is not merely performing some action, it is returning a value. So the expression on the right-hand side has a value: it is the emphasized word.

Passing a reference to a mutable object

If we wrote code analogous to the broken code in Example 3, but with a mutable type, it wouldn’t work either. For example:

# Example 5.

def emphasize(lst: List[str]) -> None:
    lst = lst + ['believe', 'me!']

if __name__ == '__main__':
    sentence = ['winter', 'is', 'coming']
    emphasize(sentence)
    print(sentence)

This code prints ['winter', 'is', 'coming'] for the same reason we saw in Example 3. Changing a reference (in this case, making lst refer to something new) is not the same as mutating a value (in this case, mutating the list object whose id was passed to the function). This model of memory illustrates:

variables

The code below, however, correctly mutates the object:

# Example 6.
def emphasize(lst: List[str]) -> None:
    lst.extend(['believe', 'me!'])

if __name__ == '__main__':
    sentence = ['winter', 'is', 'coming']
    emphasize(sentence)
    print(sentence)

This is the state of memory immediately before function emphasize returns:

variables

Here are some things to notice:

Moral of the story

The situation gets trickier when we have objects that contain references to other objects, and you’ll see examples of this in the work you do this term. The bottom line is this: know whether your objects are mutable—at each level of their structure. Memory model diagrams offer a concise visual way to represent that.