Topic 3: How Variables are Handled in Python

Before Getting Started...

Python has several basic data types such as integers, floating-point numbers, strings, lists, tuples, and dictionaries. Before reading this page it would be a good idea to make sure that you have a good handle on what those data types are, and how they are generally used.

Here are a few links to good pages to get a refresher on the Python data types.

I should probably provide a warning. If you have any programming experience in a language where 'variables' are tangible entities, that are created in a physical sense, which have a knowable address, and are assigned values, then this topic discussion could be unsettling. In the movie "The Matrix" there is a scene where "Neo" (played by Keanu Reeves) is trying to bend a spoon with his mind. One of the other candidates says "Do not try and bend the spoon, that's impossible. Instead try to realize the truth. There is no spoon."

Who would have thought that movie line would be so applicable? In Python, like the spoon, there are no variables. At least there are no variables in the 'physical sense' in which they exist in other languages like C. The 'variables' that we program with in Python are just 'convenient labels' that the Python interpreter associates with actual 'objects'. When the code is executed, the interpreter replaces the 'convenient label' with the actual 'object', and it is the type of 'object' that determines how Python behaves. It has nothing at all to do with what we call 'the variable'. We'll explore this concept below.

What really happens when a Variable is Created?

In a traditional programming language like C, when a variable is created, a specific location in memory is reserved which has a size just large enough for the data type that the variable being created is intended to hold, and that location in memory has an address. It's like putting a 'container' on a shelf, and assigning a value is like putting something 'inside' the container. If the location of the container is provided to a function, that location can be used to peek inside to see the value, or even to remove the value and replace it with another value. The important thing is that the variable is what exists, and values are just things that you put inside of them.

In Python, all of that is backwards. The values exist, and the variables do not. This is not an easy concept to grasp. Initially, I'll start the discussion using common terminology like, 'create a variable', or 'set its value', but eventually it should become obvious that these terms are wrong for Python.

Python has a built-in function called id() that can be used to explore what happens when variables are 'created'. It returns the "identity" of an object. The "identity" is an integer value that is guaranteed to be unique and constant for the object during its lifetime. The definition also states that two objects with non-overlapping lifetimes may have the same id() value. OK... That's clear as mud. The first question is what is the 'object' that this is talking about? Most people would likely guess that it would be the identity if whatever object was being passed to the id() function. That actually turns out to be correct, but not in the way you're probably thinking.

Let's explore some examples.

In the first example, a variable named var is created and assigned an integer value of 15. Then, a print() statement is used to display the value of var, and the "identity" (unique integer value) that is returned when the variable var is passed to the id() function.

var = 15
print("Value: {}, Id: {}".format(var, id(var)))

Value: 15, Id: 1696844128

So far so good. That all seemed reasonable. Let's change the example a little so that var starts with a value of 15, but is then incremented twice by 5, printing the value and id at each step.

var = 15;  print("Value: {}, Id: {}".format(var, id(var)))
var += 5;  print("Value: {}, Id: {}".format(var, id(var)))
var += 5;  print("Value: {}, Id: {}".format(var, id(var)))

Value: 15, Id: 1696844128
Value: 20, Id: 1696844208
Value: 25, Id: 1696844288

Here's the first indication that something doesn't work the way most people would expect. The value of var was changed from 15 to 20, and again from 20 to 25, but in all cases the variable was an integer type, so why would Python change it's memory location in addition to changing the value? The next example will start shedding some light on what is going on. We'll add to the example decrementing back down to 15.

NOTE: In the paragraph above I referred to the returned "identity" as an address to a memory location. This is actually a common misconception. Python's id() function seems like it provides a way to know an objects memory location, but this is not technically correct, or can't be relied upon to be correct. Unless you are running a special version of Python called CPython, where these "identities" are actual addresses, they are just unique integer values.

var = 15;  print("Value: {}, Id: {}".format(var, id(var)))
var += 5;  print("Value: {}, Id: {}".format(var, id(var)))
var += 5;  print("Value: {}, Id: {}".format(var, id(var)))
var -= 5;  print("Value: {}, Id: {}".format(var, id(var)))
var -= 5;  print("Value: {}, Id: {}".format(var, id(var)))

Value: 15, Id: 1696844128
Value: 20, Id: 1696844208
Value: 25, Id: 1696844288
Value: 20, Id: 1696844208
Value: 15, Id: 1696844128

Take a close look at the last three numbers of the returned "identity" values. That's right. When the value of var is assigned a value that it had been assigned previously, the "identity" value returned is also the same as it had been previously as well. This would suggest that the "identity" being returned corresponds to the value of the variable, and is not the "identity" of the variable itself. Let's prove that. Once again, let's create a variable called var, assign it the value 15, and print its value and "identity". Then let's use another print statement to just directly print the value of the number 15.

var = 15
print("Value: {}, Id: {}".format(var, id(var)))
print("Value: {}, Id: {}".format(15, id(15)))

Value: 15, Id: 1696844128
Value: 15, Id: 1696844128

They are the same! Let that sink in. When I started writing the code for this example, I didn't think it would even run. After all, the function prototype for id() indicates that you have to pass it an 'object', and in my C brain, the number 15 is a value, not an object. In Python however it is an object. The variable var however doesn't seem to actually exist. There is no way to have id() return an "identity" for the variable var itself. This turns out to be an important observation which holds true for all of Python's immutable data types.

Let's explore an example using a mutable data type like a list to see the difference. In the following example, we'll create a variable and assign it the values 15, 20, and 25, and print the "identity" of the variable each time. We already know that Python will create 3 unique "identities" for each of the 3 values of 15, 20, and 25. Then, we'll create a list which contains the values 15, 20, and 25, and we'll print the "identity" of the list, and the "identity" of each element.

var = 15;  print("var: {}, Id(var): {}".format(var, id(var)))
var = 20;  print("var: {}, Id(var): {}".format(var, id(var)))
var = 25;  print("var: {}, Id(var): {}".format(var, id(var)))

lst = [15, 20, 25]
print("\nlst: {}, Id(lst): {}\n".format(lst, id(lst)))

print("lst[0]: {}, Id(lst[0]): {}".format(lst[0], id(lst[0])))
print("lst[1]: {}, Id(lst[1]): {}".format(lst[1], id(lst[1])))
print("lst[2]: {}, Id(lst[2]): {}".format(lst[2], id(lst[2])))

var: 15, Id(var): 1696844128
var: 20, Id(var): 1696844208
var: 25, Id(var): 1696844288

lst: [15, 20, 25], Id(lst): 43015504

lst[0]: 15, Id(lst[0]): 1696844128
lst[1]: 20, Id(lst[1]): 1696844208
lst[2]: 25, Id(lst[2]): 1696844288

Notice that unlike var, to which we only assigned immutable values, id() does return a unique "identity" for the list object assigned to lst, which is different from the identities returned for any of it's elements. Also notice that the elements of the list are just immutable integer types, and the id values returned for each element are the same "identity" values for the integer objects that were created when we assigned those same values to var. Even though the list itself is mutable, it can contain both immutable and mutable objects. This will be important later.

The list of common Python basic types that are considered immutable are: boolean (bool), integer (int), floating-point (float), a read-only list (tuple), and a string (str). Two of these types, strings and tuples, probably deserve some examples before diving further into mutable types like lists.

First let's look at strings. In C, a string variable has a physical location, and its value can be 'changed', however when the memory is initially allocated for the string variable the size of the maximum length string has to be known to be able to allocate enough memory. In Python, you don't have to worry about that. Every time a string variable is assigned a new value, a 'new string object' with that value is created, and the variable now refers to that new object. You may be thinking to yourself, does that mean that every unique string I've ever used is just hanging around taking up memory somewhere? The answer to that would seem to be yes.

In the example below, a variable is assigned one string value, then a second different string, and is finally assigned the value of the first string again. Like the example with the integers, each unique string value results in the creation of a new string value object. When the string variable is assigned a string value equal to an existing string object, it refers back to that previous object again.

str = "The first string"
print("Value: {:20s} Id: {}".format(str, id(str)))
str = "a different string"
print("Value: {:20s} Id: {}".format(str, id(str)))
str = "The first string"
print("Value: {:20s} Id: {}".format(str, id(str)))

Value: The first string     Id: 61960272
Value: a different string   Id: 61960368
Value: The first string     Id: 61960272

Next let's take a look at tuples. A tuple is essentially a 'read-only' list. Unlike a list, where the value of an element can be changed without changing the "identity" of the list, a tuple can't be changed in any way without changing the "identity".

In the example below, let's first examine the case of a list. The example starts by creating a list with the values [1,2,3], and prints the list's "identity". Then the list is appended to add 4 and 5, and each time the list's "identity" is printed again. Notice that in all cases, the "identity" of the list remains constant. Then we throw in a curve-ball and assign the values of the list to exactly what they already are, only we do it explicitly using lst = [1,2,3,4,5], and although the value looks the same, from Python's point of view, we just assigned a completely new value object. Notice that the "identity" is no longer the same. One last item, the list is explicitly assigned to it original value using lst = [1,2,3]. This will blow your mind. The "identity" of the list is now equal to what it was initially, even though we had added 4 and 5 to it.

lst = [1,2,3];   print("Value: {} Id: {}".format(lst, id(lst)))
lst.append(4);   print("Value: {} Id: {}".format(lst, id(lst)))
lst.append(5);   print("Value: {} Id: {}".format(lst, id(lst)))
lst = [1,2,3,4,5];   print("Value: {} Id: {}".format(lst, id(lst)))
lst = [1,2,3];   print("Value: {} Id: {}".format(lst, id(lst)))

Value: [1, 2, 3] Id: 54388456
Value: [1, 2, 3, 4] Id: 54388456
Value: [1, 2, 3, 4, 5] Id: 54388456
Value: [1, 2, 3, 4, 5] Id: 50355536
Value: [1, 2, 3] Id: 54388456

NOTE: After some additional experimentation, the observation that re-assigning the list to the original values results in the original "identity" is not always true, and should not be counted on. This appears to be a result of some internal optimization, and is probably a side-effect of the example programs being so simple and predictable. Additionally, something more than what is made obvious using id() is happening in the background to have removed the changes made to the list without changing the "identity", or Python tracks mutable object changes in a way that is not exposed.

The case of the list was special in that it was possible to modify the values of the list using member functions of the list like append(). Because the tuple is read-only, those member functions don't exist for a tuple. Therefore, you have to explicitly assign a complete value for the tuple every time, and like the list, that will result in a completely new value object and a new "identity". If however, an exact combination of values is ever repeated, Python will re-assign the original value object instead of creating a new one.

tpl = (1,2,3,4,5);   print("Value: {} Id: {}".format(tpl, id(tpl)))
tpl = (6,2,3,4,5);   print("Value: {} Id: {}".format(tpl, id(tpl)))
tpl = (7,6,3,4,5);   print("Value: {} Id: {}".format(tpl, id(tpl)))
tpl = (1,2,3,4,5);   print("Value: {} Id: {}".format(tpl, id(tpl)))

Value: (1, 2, 3, 4, 5) Id: 41482304
Value: (6, 2, 3, 4, 5) Id: 46554064
Value: (7, 6, 3, 4, 5) Id: 46670592
Value: (1, 2, 3, 4, 5) Id: 41482304

One final example before moving on. In this example we will create two different tuples containing 5 integer values, with some values different and some the same. The "identity" of the tuples will be printed, which will be different. Then the "identity" of two inner elements with the same values will be printed, and they will be the same.

tpl1 = (1,2,4,8,9);   print("Value: {} Id: {}".format(tpl1, id(tpl1)))
tpl2 = (6,3,8,1,2);   print("Value: {} Id: {}".format(tpl2, id(tpl2)))
print("Value: {} Id: {}".format(tpl1[1], id(tpl1[1])))
print("Value: {} Id: {}".format(tpl2[4], id(tpl2[4])))

Value: (1, 2, 4, 8, 9) Id: 46625344
Value: (6, 3, 8, 1, 2) Id: 46625392
Value: 2 Id: 1535953040
Value: 2 Id: 1535953040

What this tells us is that the tuple isn't stored as the full unique set of values for each unique tuple combination. It is being stored as pointers ("identities") to the individual value objects that are created for each unique immutable value. So even though each list contains 5 values, the values 1,2,8 are common to both, so both tuples have pointers to the same objects for those common values. I'll leave this up to you, but if you test something similar for the characters in various strings, you should find that all occurrences of a single letter, like 's', all point to the same object as well.

Summing up what we've learned

When we tell Python to change the value of a variable associated with an immutable object (like integer value 25) it can't. The 'variable' doesn't actually exist in a tangible sense, so talking about changing 'its value' is nonsensical. The Python interpreter will create a new 'value object' instead and associate its "identity" with the variable name.

The variable name is just a 'convenient label'. The Python interpreter deals directly with the 'value object' that is associated with that label. This acts very similarly to the compiler directive #define in C, where the compiler replaces the 'label' with the 'object' in every place that the label exists in the code.

The "identity" returned by id() when it is passed a variable name that is associated with an immutable object, is the "identity" of the immutable object.

The "identity" returned by id() when it is passed a variable name that is associated with an mutable object, is the "identity" of the mutable object.

The difference between mutable and immutable objects are that mutable objects act like a container where all of the changes made to the mutable object are maintained. The "identity" of a mutable object acts like a pointer to that container.

Everything above can be simplified using common terms in the following way. Python always follows these rules:

Making Copies of Variables

Unfortunately the two rules above don't seem to be made very clear in the classes, guides, or documentation that you will find for Python, and it is an area where people are frequently surprised by what Python does. What is worse, is that there is a lot of mis-information on the Internet about how to solve the unexpected behaviors. Hopefully, having seen the examples above, this next section won't be so confusing.

In the first example, we create a variable var1 and assign it a value of 25.432. Then we create a second variable var2 and assign it to be equal to the first variable var1. Since we know that 25.432 is an immutable object, we also know that the assignment of var2 will be done using 'pass by value', so it is directly assigned to the value object 25.432. When we print the id() value, we can see that both variables refer directly to the same immutable value object.

var1 = 25.432
var2 = var1
print("var1: {} id(var1): {}".format(var1, id(var1)))
print("var2: {} id(var2): {}".format(var2, id(var2)))

var1: 25.432 id(var1): 42216304
var2: 25.432 id(var2): 42216304

To prove that there is no 'linkage' between the two variables, let's repeat the example, but this time we will modify the value of var2 before printing the output. Since var2 references an immutable object, meaning that it can never be changed, Python creates a new value object for the new value of 34.769, and changes var2 to reference that new object. It does not change the value of var1 in any way.

var1 = 25.432
var2 = var1
var2 = 34.769
print("var1: {} id(var1): {}".format(var1, id(var1)))
print("var2: {} id(var2): {}".format(var2, id(var2)))

var1: 25.432 id(var1): 42216304
var2: 34.769 id(var2): 42216352

That should make sense. As long as we are assigning a variable to be equal to another variable that references an immutable object, the value of the variable being assigned will always be equal to the value of the immutable object. There will never be any linkage between the two variables.

Let's repeat a similar set of examples with a mutable object, using a list. In this example, we create a variable lst1 and assign it to a list of [1,2,3]. Then we create a second variable lst2 and assign it to be equal to the first variable lst1. Since we know that the list [1,2,3] is a mutable object, we know that the assignment of lst2 will be done using 'pass by reference'. When we print the id() value, we can see that both variables refer to the same mutable object. So far this 'looks' the same as the immutable example. What is not obvious yet, is that the mutable object "identity" acts as a pointer to the container for the mutable object.

lst1 = [1,2,3]
lst2 = lst1
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))

lst1: [1, 2, 3] id(lst1): 48057840
lst2: [1, 2, 3] id(lst2): 48057840

To prove that there is a 'linkage' between the two variables, let's repeat the example, but this time we will modify the value of lst2 before printing the output. Since lst2 references a pointer to the same mutable object as lst1, making changes to lst2 is going to also affect the value of lst1. To make sure there is no question of what's happening, the list and its identity will be printed both before and after the change.

lst1 = [1,2,3]
lst2 = lst1
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))
lst2[1] = 7; print("<-- changed lst2 only here")
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))

lst1: [1, 2, 3] id(lst1): 46951240
lst2: [1, 2, 3] id(lst2): 46951240
<-- changed lst2 only here
lst1: [1, 7, 3] id(lst1): 46951240
lst2: [1, 7, 3] id(lst2): 46951240

This usually really surprises people. Keep in mind that many people, especially those with experience in other languages, think of the variable as a real object. With that mindset it doesn't make sense why Python would treat the same variable differently in some situations than in others. But, once you realize that the variables in Python don't exist in a physical sense, and that they are only 'convenient labels' to refer to objects which may be either immutable or mutable, then it makes sense why Python behaves differently. It has nothing to do with the variable, and everything to do with the object.

The key take-away is that as long as we are assigning a variable to be equal to another variable that references a mutable object, the value of the variable being assigned will always be equal to the pointer to the mutable object. There will always be a linkage between the two variables.

Copy by Value using the list() Function

There is a way to tell Python to create a 'copy' of the list instead of copying the pointer to the original list. This is done using the list() function.

To demonstrate that this works, let's repeat the previous list example, but this time we will assign the value of lst2 by using lst2 = list(lst1). The first obvious difference is that even though the two lists appear to be the same, the "identity" of the two lists starts out different. Then, after modifying one of the elements of lst2, it can be seen that lst1 remains unchanged. So, this would appear to have worked.

lst1 = [1,2,3]
lst2 = list(lst1)
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))
lst2[1] = 7; print("<-- changed lst2 only here")
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))

lst1: [1, 2, 3] id(lst1): 55205120
lst2: [1, 2, 3] id(lst2): 59553072
<-- changed lst2 only here
lst1: [1, 2, 3] id(lst1): 55205120
lst2: [1, 7, 3] id(lst2): 59553072

WARNING: This only works properly for a list where all elements are immutable! If a list contains a mutable element, the 'pointer' to that mutable element is copied and there will be a linkage between that element of the old list and the same element in the new list!

To demonstrate what is meant by the warning above, let's repeat the example, but this time use a list that contains a mixture of immutable and mutable objects. For the mutable object, a second list will be inserted inside the first list as one of the list elements.

Initially, we'll only change one of the immutable elements at list index [1]. The result is that only lst2 is changed. So far this seems to work correctly.

lst1 = [1,2,[3,4],5]
lst2 = list(lst1)
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))
lst2[1] = 7; print("<-- changed lst2 only here")
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))

lst1: [1, 2, [3, 4], 5] id(lst1): 58570632
lst2: [1, 2, [3, 4], 5] id(lst2): 58571552
<-- changed lst2 only here
lst1: [1, 2, [3, 4], 5] id(lst1): 58570632
lst2: [1, 7, [3, 4], 5] id(lst2): 58571552

Let's try that again, only this time let's change one of the elements of the embedded list at list index [2][0]. The result is that this time both lst2 and lst1 changed. So, why would that be? We clearly told Python to make a 'copy' of the list. Why is there a linkage between the lists? The answer is that Python only performs what is called a 'shallow copy' of the list at the top-level. This means that when it encountered the second list [3,4] inside the first list, it copied the pointer (the "identity") of the second list, it did not create a full copy of the second list as well. The result is that both lists, the original and the copy, both point to the same secondary internal list.

lst1 = [1,2,[3,4],5]
lst2 = list(lst1)
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))
lst2[2][0] = 7; print("<-- changed lst2 only here")
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))

lst1: [1, 2, [3, 4], 5] id(lst1): 54156624
lst2: [1, 2, [3, 4], 5] id(lst2): 58523856
<-- changed lst2 only here
lst1: [1, 2, [7, 4], 5] id(lst1): 54156624
lst2: [1, 2, [7, 4], 5] id(lst2): 58523856

Deep Copy by Value using the deepcopy() Function

There may be times when the effect of the shallow copy is desirable. For example, if you want some items of a list to be independent between copies, and some items to be shared by all copies. However, if the desire was to create a truly independent copy, where the only thing in common is the structure and initial values, there is a way to tell Python to do that. This requires importing the copy library, and using the deepcopy() function.

The previous example is repeated, this time using the deepcopy() function. The result is that this time when lst2 is changed, only lst2 changes. Success! We have independence.

import copy
lst1 = [1,2,[3,4],5]
lst2 = copy.deepcopy(lst1)
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))
lst2[2][0] = 7; print("<-- changed lst2 only here")
print("lst1: {} id(lst1): {}".format(lst1, id(lst1)))
print("lst2: {} id(lst2): {}".format(lst2, id(lst2)))

lst1: [1, 2, [3, 4], 5] id(lst1): 48505400
lst2: [1, 2, [3, 4], 5] id(lst2): 44391760
<-- changed lst2 only here
lst1: [1, 2, [3, 4], 5] id(lst1): 48505400
lst2: [1, 2, [7, 4], 5] id(lst2): 44391760

In the examples above, we've only discussed lists as mutable objects. The same holds true for other mutable objects, such as dictionaries. Shallow copies of dictionaries can be made using the built-in dict() function, the same way that list() worked for performing a shallow copy of other lists.

Now that we've introduced the copy library, there is also another option available. Along with the deepcopy() function, there is also a regular copy() function as well. The copy() function performs a 'shallow copy'. There are two potential benefits to using copy() as opposed to the built-in shallow copy functions for each mutable type like list() or dict(). First, is that it is obviously different than the built-in methods, which may prompt someone to go look it up and find out what the differences are. Second, copy() can be used to copy any compound object.

Passing Variables to Functions

In the previous sections, we looked at setting variables to be equal to other variables or to copies of other variables. When a variable is passed to a function a similar process occurs between the variables whose scope is inside the function, and the variables whose scope is outside the function.

In the next examples we'll explore what this means. However, since none of the previous topics have covered functions, it might be nice to read up on functions before we get started. Here is a nice page for getting a refresher: w3schools: Python functions tutorial with examples

The example below has 3 sections separated by blank lines. In the first section, 3 variables a, b, and c are assigned immutable integer values of 25, 30, and 35. In the second section, a function called inc_and_print() is defined which accepts 3 arguments, which are labeled x, y, and z. These labels become new variables of the same names that only exist within the scope of the function. When the variables are created they are assigned the values equal to the objects passed to the function in the function call. Finally, in the third section, 3 function calls are made which print the global scope variables a, b, and c, then call the inc_and_print() function, passing it the arguments of the global scope variables a, b, and c, and finally, another call to print the global scope variables a, b, and c.

It is important to understand that when variables (a,b,c) are passed to the function with arguments defined as (x,y,z), that Python will perform the following assignment of variables: x=a, y=b, and z=c. This assignment will follow all of the same rules that we discussed above.

Since the variables being passed to the function represent objects that are immutable, the assignment is made as 'pass by value', and there is no linkage between the local scope variables and the global scope variables. This can be seen in the output where the global scope variables are the same before and after the call to inc_and_print(). From the print statement inside the function it can be seen that x, y, and z were assigned the 'values' of a, b, and c, and were each incremented by one prior to the print statement.

a=25; b=30; c=35

def inc_and_print(x,y,z):
    x=x+1; y=y+1; z=z+1
    print(x,y,z)
    
print(a,b,c)
inc_and_print(a,b,c)
print(a,b,c)

25 30 35
26 31 36
25 30 35

Let's try the same example again, only this time the function is re-written slightly to accept a mutable list as the argument.

This time the variables being passed to the function represent objects that are mutable, so the assignment is made as 'pass by reference', and there is a linkage between the local scope variable and the global scope variable. This can be seen in the output where the global scope variables are the not the same after the call to inc_and_print().

a=[25,30,35]

def inc_and_print(x):
    x[0]+=1; x[1]+=1; x[2]+=1
    print(x)
    
print(a)
inc_and_print(a)
print(a)

[25, 30, 35]
[26, 31, 36]
[26, 31, 36]

From everything that we learned above, the Python behaviors for passing mutable and immutable objects to a function shouldn't be a surprise.

To re-iterate the governing rules:

The obvious questions are, what if you want to modify the value of a variable holding an integer value when it is passed to a function, or what if you don't want a list you pass to a function to be modified by that function. Let's examine the second question first, because it is the easier of the two.

The obvious choice would be to pass the function a copy of the list using copy.deepcopy(). This has the effect of breaking the linkage between the local scope variable and the global scope variable. Even though the object is still being passed by reference, that reference now points to a copy made specifically for the function to use. Now, even though the function sorts its copy of the list, that change is no longer reflected in the global scope variable.

data = [4,5,-12,57,5,6,5,7,3]

def print_median_value(d):
    print("Before:    d = {}".format(d))
    d.sort(); m = d[int(len(d)/2)]
    print("After:     d = {}".format(d))
    
print("Before: data = {}".format(data))
print_median_value(copy.deepcopy(data))
print("After:  data = {}".format(data))

Before: data = [4, 5, -12, 57, 5, 6, 5, 7, 3]
Before:    d = [4, 5, -12, 57, 5, 6, 5, 7, 3]
After:     d = [-12, 3, 4, 5, 5, 5, 6, 7, 57]
After:  data = [4, 5, -12, 57, 5, 6, 5, 7, 3]

The first question is far more difficult because it doesn't conform to the fundamental way that Python operates. The variables don't really exist in Python, only the objects that they represent. Therefore the concept of 'passing a variable' no matter what its object is, doesn't make sense. If you really have a need to do this, then you have to 'wrap' the variable inside of a mutable object. That is the only way that Python will allow you to 'pass by reference' an immutable object. The following simple example shows how an integer value, wrapped in a single value list, can be passed to a function in a way that its value can be modified by the function.

var = [3]

def save_value_to_var(lcl_var, lcl_val):
    lcl_var[0] = lcl_val
    
print("Before: var = {}".format(var))
save_value_to_var(var, 7)
print("After:  var = {}".format(var))

Before: var = [3]
After:  var = [7]

If there were several such 'variables' that needed to be passed into functions to be modified, Another interesting way to implement wrapping them would be to place them in a dictionary. In the following example, an empty dictionary named var is created using the curly braces {}, and then three 'variables' are added to the dictionary using variable names as the key, and an integer value as the value. A function called inc_var is defined, which takes the name of a dictionary, a string object (representing the variable name key), and an integer to increment the variable value by. The dictionary is printed before calling inc_var, and you can see that the 'variables' and their 'values' correspond to the initial values assigned. After making calls to inc_var, the dictionary is printed again, and the variables can be seen to have changed by the function calls.

var = {}
var["avg"] = 5
var["min"] = 2
var["max"] = 12

def inc_var(x,y,z):
    x[y]+=z
 
print(var)
inc_var(var, "avg", 3)
inc_var(var, "min", 7)
inc_var(var, "max", -6)
print(var)

{'avg': 5, 'min': 2, 'max': 12}
{'avg': 8, 'min': 9, 'max': 6}

NOTE: Because there is no Pythonic way to 'pass by reference' a variable that is expected to hold immutable objects (like integers or floating-point values) to a function, some method must be devised to wrap such variables for this purpose. That also means that each function being passed such values must understand how to address the variables within the wrapper. If you use a list, that requires one method. If you use a dictionary, that requires another. First, decide if you really need to do this. If you must, and you're on a team of more than one, make sure everyone agrees to the methodology.




That's it for now. Just remember... There is no spoon!




Additional Resources