 In this video, we're going to see how the list data type is prone to lead to a lot of semantic errors if you don't know what you're doing. And this has to do with the fact that it is a mutable data type. So let's look at a couple of examples to see a very common source of confusion for students. So we're going to talk about lists as arguments to functions, of course. So we are going to make a very easy example here to illustrate some points. So let's go ahead and create a list called letters and let's simply put in there a couple of letters like a, b and c. Okay, so let's also write a function and let's call the function at x, y, z. And the function will do what its name suggests. It will simply take an argument in the list and it will simply add x, y and z to the end of the list of the argument. So the function needs to take an argument. Let's simply call it arc short for argument. And let's also give it a dog string, even though this is probably only a very short example function. And we're going to write append letters to a list. Just to illustrate a point. Otherwise, there is no point in writing such a function because otherwise you would simply use either the append or maybe the extend method depending on what argument you're given. So these methods already exist, but let's write a function that does that. So the function takes an argument as its input and it is simply going to extend onto that a list with the letters x, y and z. So in other words, they get appended to the end of the list. And then we're going ahead and we will say return arc because the list object that results after we appended the x, y and z, we also want to return it. So let's for now simply write return arc. That's what we're used to. The function should have some output. So let's go ahead and call the function. So let's go ahead and call the function at x, y, z and give it letters as the argument. Okay, so let's go ahead and let's simply execute that. I was just wondering if I should capture the return value in an own variable. Let's not do that. Let's simply execute the function once. So now what we see is the function indeed has a return value. We see that as output below the code cell and it says it shows us the list a, b, c and then x, y, z. So now just by looking at the order of the cells here, what should the letters variable be? Okay, should it be a list a, b, c or should it be the list a, b, c, x, y, z? So from our normal understanding of how we want to use functions. So remember when I introduced the concept of a function in this course, I said a function is really a specified input-output relationship. It's like a promise. We give the function something, we get something back. So here we give the function something, an argument, a list, and we get back a list. And it's basically a list that has three additional characters in it. So in other words, what our idea of viewing a function as a mere input-output relationship suggests is that the input to a function should not change when we call it a function. Instead, we should get back some output that is unrelated to the input. Okay, we want to keep those things separate. So let's look at letters. And unfortunately, we'll see that letters also has x, y, z to it. And for those of you who understand probably the extent method and everything are quite well, for those, this should not be a big surprise. I'm giving an argument. I'm extending it. So I'm modifying the argument and I'm simply returning it. So in other words, even though the idea of a function suggests to us that there is an input and an unrelated output, this promise is spoken here. The input is the output. So in other words, the input is modified and we get back the modified input as the output. We don't get back an independent output. Let's also look at your favorite tool, Python Tutor. And let's see how that looks like in a memory diagram. So let's first copy paste over the list letters. And then second, let's copy paste over the function. And then last but not least, let's also call the function. Let's maybe do it like that. And let's also here in this version here, let's capture the return value of the function call into a variable. Let's call it letters. And let's simply call it with XYZ. So let's go ahead and execute that. So what's going to happen is first we create a list letters in the global scope. It has ABC in it. You remember that the ABC, the string objects are not really inside the list. They're really somewhere outside here and we have references in the list because lists only have references in them. Next, we simply create the function. So the function has its own object here containing all the code that is put inside the function. And now next we are going to call the function and we get a function scope called at XYZ here. And we see that as the function is being executed, it takes a parameter called arc, which references the same list letters that we provide as the input. So in the very first video where I talked about these kind of surprises, the video was called Who Am I and How Many, we already saw that having more than one reference to the same list object could lead into very weird situations. And in the other video that we did a couple of videos ago, shallow versus deep copies, we also saw that if we make a copy of a list, we usually get back a shallow copy and a shallow copy basically means we have two lists pointing at the same inner objects. So in other words, what I'm saying is that the situation where we have more than one reference to the same object happens so often in Python. It is almost natural that it happens all the time. However, we must not forget that if we have more than one reference to one object, if we make changes to this object, then we can see the changes from both perspectives, from via both references, right, because there is only one object. So if we continue here and we execute the first line in the function's body, what we simply do is we extend XYZ to the only list that is in memory. Now, next, we are going to return the argument. So we are going to return the input and we store it under a variable called letters with XYZ. So we get the return value. The return value as we see is also just a reference to the list here, to the input list. And now after the function is done, we have two variables called letters and letters with XYZ that both reference the same list object. So in other words, that should not be confusing actually, because if you know what is going on in memory, there is nothing to really be confused about. You just simply know that whenever you call a function, you remember that the input to a function is always passed in by reference. So we never get a copy. We never get like a real deep copy, so to say, of the data. When we call a function in Python, all the arguments are always passed in by reference. Okay, so in other words, what we see here in the Trooper Lab should actually not be surprising if we think about it. However, from the storyline, it is a bit surprising because in the way of how we should think about functions and we should think of functions as black boxes, as simple promises of an input-output relationship, we give it some input, we get back some output and the input, of course, remains unchanged. That is the ideal scenario of a function. So what could we do with this function here to basically make it stick to the way we want to treat functions? So first of all, let's give this a title here. So let's call these kind of functions, so called modifiers. So modifiers are simply functions that modify the input and give back the input. Now let's do the same example again with a different approach and we will call that pure function. And pure functions, they are pure in the sense that the word pure means the input-output relationship is pure in the sense that the input is not changed. So we give it input and we get back an output and no matter how often we call the function for the same input, we get back the exact same output. So what do I mean by that? I just said maybe the most important part here. When I call a function many, many times with the same input, I always should get back the same output. However, if I go ahead here and I call the add xyz function a second time, I now get back a different input, so to say. And the reason why is because we changed letters throughout. So this is now a code like this may easily result in a situation where you are confused about what does the code actually do. But now let's rewrite that. So let's first go ahead and create letters a second time. So we have like a new letter variable here. And now let's also go ahead and rewrite the function. So what are we going to do here is we're going to name the function of course also add xyz. It also takes an argument and let's also write a doc string here. And now let's be a bit more precise and simply write create a new list from the existing one and add xyz. So how could we implement that to shut the function? So we could do the following. We could go ahead and inside the function body. The first thing we do is we take the input and we make a copy. Okay. And then we only work on the copy and we never work on the actual input. So how do we get a copy in Python? There are a couple of ways. So one way would be to assume that arc is a list and we can assume that here and use the list method copy. Which gives us back a shallow copy. Or the other approach would be to go ahead and use the indexing notation and take the full slide. We have learned the full slide is basically the same as the copy method. And many Python programmers prefer that actually because the bit shorter and it just got popular. So let's go ahead and let's say we create a variable that's called it new arc. Could also use some other name, but the name is not so important here. And we use the name new arc to capture the result of the full slice. So you remember from the video shallow versus deep copy that we get a shallow copy here. So in other words, both the list arc and the list new arc refer to the same objects with the same ABC objects, the string objects among them. However, that is not a problem here because A, B and C are strings and strings as we learned are immutable. So the problem that we discussed in the video of shallow versus deep copies only exists if the data that is shared among two lists is mutable. If the data that is shared among two lists is immutable, we won't ever have a problem really. So and because we know that A, B and C are immutable string objects, we can simply take a shallow copy here. No big problem. Then we go ahead and we will say new arc dot extend. And we will also extend that with X, Y and Z. And then last but not least, we will go ahead and we will say return new arc. So now let's see how this function is different. So let's go ahead and let's first call the function, just the new version at X, Y, Z. And let's give it letters and I get back ABC, X, Y, Z, exactly what we want to get back. If we now go ahead and check what is letters, then we'll see that letters is still ABC. That was different above. Above letters was also ABC, X, Y, Z. Now if I make the same function called a second time with the same input, I get back the same output. And that's what we want from a function. A function should, given the same input, always give back the same output, assuming it's a non-random function, of course. So otherwise, you know, it will become very hard to reason about code because if the function for the same input gives back some other output, then it's really very hard to understand the program. So that is a minor difference, but the difference is also the learning, the big learning from here is that whenever we call a function, we have to remember the arguments are passed in by reference and when we pass in a mutable object and we know for sure it's a mutable object like a list, for example, then it is a good idea to first make a copy of it. And then when we make the copy, usually we can get away with a shallow copy. So we get away with a shallow copy mainly if we know that the data in the list is immutable. But in some situations, maybe you need a deep copy here. You need a deep copy if you know the list itself will contain other mutable data. So that is an improvement. Let's also quickly see how that changes in Python Tutor. So let's simply overwrite the function definition here and let's visualize the code right here. So we have our letters list, ABC. We create our function at XYZ. We call the function. We get now a reference to the outside world. We cannot change that. This is basically built into Python. So whenever we call a function, the function will always be given references to the outside world. But then comes something interesting. In the first line, what we do is we create a copy, right? And remember Python Tutor simplifies the visualization a little bit. So the A here and the A down here is actually the same A. The B and the B is actually the same B and the C and the C here is actually the same C. So just imagine the A, B, and C were floating around in between and both lists would basically reference the same A, B, and C objects. So that is how you should think of that. And now let's go on and execute the second line here where we extend XYZ and we will simply extend XYZ into the new list in between that we created by copying. And now we are going to return that list as the return value. And our variable letters with XYZ is now different from our original letters. And we call that a pure function. So a pure function does not change the input. A pure function will always give you back the same output for the same input and also a pure function will never change the input. So that is a big difference to what I call a modifier function. There will be better names once we talk about object orientation in Chapter 11. But for now, simply note that we don't want to talk about object orientation and methods, how we implement our own methods yet. So for now we simply call it a modifier function, modifier meaning it modifies the input. Okay. So there is of course a better way to do the modifier. So maybe let's make a new Subjector up here. And let's say better way. So let's assume you don't want to make a copy. So one downside of this approach, as we see in the memory diagram here, it uses more space of course, right? If we have two list objects, we have more memory occupied on our computer than if we only have one list that gets modified. So sometimes maybe intentionally you want to build code such that it modifies the input. Then what you should do is the following. Let's go ahead and copy our letters here. And now let's also go ahead and rewrite the function. So I'm going to copy paste the solution up here. And now there's only one thing I will make different to above. And the thing is the confusing part. The confusing part is that this function up here has a return value. So remember how I said when we talked about list methods, I said to you that when a method changes an object in place, then what you want is you don't want any output. You don't want a return value. That is why the list methods had no return value. In comparison to that, the string methods, because strings are immutable, they would have always a return value. So if you had the dot lower, the upper method, they would always return a new string object. But the list methods, like append, extend, remove, and pop, and all the others, they usually don't have the list method that do change the state inside the list. They never have a return value. So how can we do that? Well, the easiest way would probably be to simply get rid of the return line at all. So this would simply not return anything, so it would return none. But sometimes you want to be a bit more explicit, so you could also say, for example, return none explicitly. But if you don't write return none and there is no return statement in a function, then the return none is implicit. It will still happen. But now I leave it in here explicitly so that you can see it when you refute the materials. Now let's define this function. And now we are going to go and do the following. I will do the following. I will say add x, y, z. And I will give it letters. And now I don't get a return value. So we see no output below the cell. And that is a convention for us to tell us, OK, if there is no return value, then the input has changed. And then let's look at letters. And letters indeed has changed. And if we call add x, y, z one more time, then again we get no output. And therefore, if we look now at letters, we have x, y, z twice at the end of the list. So now this is the way it should be if you write a function in a way that modifies the input. However, a good practice is in the beginning, you should always make sure that your function does not change the input. So I'm not saying that changing the input to a function is a bad thing. It's a thing that you should do on purpose. And the best way to learn about when is a good purpose and when it's not a good purpose to do that is to wait until chapter 11 when we talk about object orientation and then learn about what methods are for. So in other words, I told you before many times that methods are functions that are attached to objects. And in particular, what methods are, maybe now I can give you a next piece of information regarding methods. So methods are in particular functions that change the state of an object that is mutable. So that is one way of saying it. So if you go into object orientation, then one purpose that methods have is to change the state of the objects on which they are attached. So that will be one of the learning outcomes in chapter 11. But for now, we don't know what methods are. We don't know what object orientation is really. We know kind of what it is. It's the paradigm after which Python models its memory. But we have not yet looked at any of the object-orientated features of Python. So for now, we can only work with functions, like real functions that are not attached to an object. And we should always in our heads at least ask the question, does the function take input? If so, should the input be modified? And if so, why? Why would that be a good thing to do? And if not, then a good practice is always in the beginning to make sure that you work only on a copy of the data and then also make sure that you understand the difference between a shallow and a deep copy. And usually you can get away with a shallow copy, but sometimes using a deep copy is necessary. So that is it. So pure functions are pure because they don't change the input. That's important. So that is the main learning in this video. And also remember, there is nothing you can do to prevent this. You have to understand how you design your code, your functions, the code inside your functions. Because as we see in Python, when we call a function, every time we call a function, we get a reference to an object that exists outside as well. So therefore, this happens naturally. There's nothing we can do about it. Therefore, we must know how we design the function internally. So I will see you in the next video where we talk about another data type called a tuple. And the tuple data type is like a list, but it cannot be changed. It's immutable. And then many of the problems that we see here that we may have with list objects will go away. So I will see you in the next video.