 Welcome back to chapter 7 of the Introduction to Python and Programming course. This is actually part 1 of chapter 7. There will be a second part. Both parts will concern sequential data. So what is sequential data? As business people, sequential data is most of the data we work with. So any data that you commonly would work with in a spreadsheet software like Excel or so is at the end of the day sequential data. A mathematical term for sequential data would be, for example, the data that is typically available in linear algebra. So we're talking about matrices and vectors. So we will now see many, many data types that are helpful in modeling sequential data. In this first part, we will look at data types that are built in a way that they keep all the data in memory simultaneously. And then in the second part of chapter 7, we will look at the map filter reduce paradigm and in a way that allows us to work in a very memory efficient way by only keeping only the necessary data in memory. So let's do the basics first here and then we do the more advanced parts in part 2. So first of all, we are now far in in the course. So it's time to talk a little bit about abstract concepts again. I have already given hints at some situations in the previous chapters, but now we want to formalize some things. So in particular, I want to talk about collections and sequences. And sequences, of course, being the abstract concept that is important for sequential data. But collections is a very related topic. Okay, so let's start with an example again. I take the numbers list that we saw in the very first example in chapter 1. Numbers from 1 to 12 are unordered and I store it as the variable numbers. Then a second example. I create a variable called text and I store the text that we saw in chapter 6, Lorem, Ipsum and so on. So now the question is, what do these two variables have in common? Well, both of them reference objects that behave very much the same in many ways. And this is exactly what we want to focus on in this section. So as we already heard a little bit about duck typing in chapter 4 and also a little bit in chapter 5, duck typing is when we don't care too much about the data type of a certain object, but we rather look at how a object behaves when we use it. So let's look at four ways in which the list here and the text behave in the same way. So first of all, we can loop over numbers. So four numbers in numbers with a for loop here and we print out the individual numbers on one line. Okay, let's do the same thing for the characters in the text string. We print all the characters with the space in between. So this seems very trivial. It is. However, the question is, why does it work? So what is the big deal here? The big deal here is that we are using the fourth statement to loop over different types of objects, namely a list object and a text string. And in many other languages, this is not possible. However, it's possible in Python. And this is because Python is a language that mostly cares about how an object behaves and not what type it is. So that's the first behavior that two have in common. We can loop over them. And we have a name for that. We've heard the same before. Any object that we can loop over, we call an iterable. Okay, then we look at another behavior that we have also seen before. So for example, for the numbers list, I can check if a certain number is contained in it. So here I check if the number zero contained in the numbers list. And it's not. I can go ahead. I can replace it by a one. And now we have a true. So the important thing is not that the number is included or not. The important thing is that we can actually use the in operator. So we are allowed to ask the numbers object, hey, do you contain this? Okay, we can also do that with text. So for example, I can ask the text string here. Do you contain the character lowercase l? And text will confirm that. So that's the second behavior. We have a name for that. The formal name for that is the container property, which means in this case, the list object, but also the string object are made up of other objects. So they contain references to other objects. This is why they are containers. And the characteristic operation for a container object is the in operator, the membership testing operator. So these two abstract concepts we have seen before. Now comes the first one. We haven't talked about explicitly, but we have also seen before. So I can use the pythons build in len function and ask Python how many elements are in the numbers list, and it will confirm to me that there are 12 numbers. I can also go ahead and ask Python, hey, how many characters are in the text string, and we get back 27. So both data types, the list data type and also the string data type, have a notion of length. So both of them are finite. So we will see in the second part of chapter seven, data objects or data types that can model an infinite stream of data. So you may wonder how can we put an infinite amount of data in memory? Well, the answer is we can't, but we can play tricks. And by playing some tricks that we will see in the second part of chapter seven, we will be able to make a computer act as if an infinite amount of data were in the machine. But so what I'm saying is there will be data types that do not have the notion of being finite. There will be infinite data types. But here the text string and the numbers list, they are finite and we have a formal word for that. And the formal word is just sized. So if you go ahead and read the text chapter on this, on chapter seven, then you will see some references to all the technical terms. And again here the technical term for being finite is called sized in the Python world. Okay, let me stay here for a bit. So now we have three properties. And whenever those three properties are available in the same data type, then we say that the data type is a collection. Okay, so it's the three properties being iterable, so we can loop over being a container, so they contain other things, and then finally they contain something finite. So what's missing here? The missing piece here is we never said that numbers and text have to be ordered. However, we already know that both numbers and text, they have an order that they come with. So the characters in text and also the 12 numbers in the numbers list, they come in an order and the order is maintained in memory. However, we will also see data types that don't have an order. And data types that fulfill the first three properties that we have just talked about, but are not ordered, they are collections. And all collections that have the fourth property of being ordered, they are called sequences. And this is why I can loop over the numbers list in reverse order. Why reverse order? Well, in order to be able to go over a list in a reverse order, there has to be a forward order to begin with, otherwise we wouldn't have a reverse order. And therefore, the technical term for this fourth property is called reversible. And just as we can loop over the numbers list in reverse order, we can also loop over the text string in reverse order. And we get back all the characters in reverse order. And this is the property again that is called reversible. And again, we have the four properties. We will soon see one slide where we have all the concepts on there on one slide. And these four ideas are very important. And whenever all the four ideas come together in a certain data type, the data type is what is called a sequence. And sequences exist to model sequential data. And when you read the Python documentation, you will see the term sequence or the abbreviation SEQ, which is often used as the argument name SEQ. You will see that a lot. And whenever you see that in the documentation, not only in the core Python documentation, but in any documentation on anything in Python, then you know whatever requires a sequence basically requires an object that fulfills these four properties. And it does not matter which type it is. And usually sequences, the most common or the two most common sequences are probably the list and the text string. But we will see some other data types as well, but there are also sequences. Okay, so that was the abstract part for this chapter. Now we will talk a little bit more about a concrete thing, namely the list type. So the list type, as I said, is a concrete example of the abstract idea of a sequence. And we have seen this starting in chapter one. So we are very familiar with the list type. And here in this chapter, I want to talk about the details of how the list type works and how it's modeled in memory and what the implications are for us and also some caveats that we should consider when we use the list type in a real-world project. So first of all, how do we create lists? Well, we can use the literal notation, open brackets, closing brackets. And if we don't put anything in between, we will create an empty list. So in my memory diagram, let's do this. So what happens is, as we have seen before, lists are these objects that we call arrays. They consist of slots. And in this case, the list is named empty. And we have a reference here. And in the examples we saw previously, we would have references going from the slots to other objects. Here we don't, because the list is empty. Nevertheless, the list is initialized with a couple of slots. How many there are is not important. You could actually assume there are no slots, but I just want to give you a more correct picture. So Python allocates some memory for empty slots so that when we work with the list later on, we don't have to create new memory and allocate new memory to it. We can just use the empty slots to put something in if you want to put something in the empty list, for example. Let's go ahead and create another list. I call it here simple. And just to illustrate the point here one more time. So the right-hand side is, of course, evaluated first. So let's do this. Put this a little bit off here. And I have to draw very small here because this picture is going to become a little bit bigger. So this is, of course, also a list type. Now Python has done that. So now the list is empty. And now Python goes back here and reads the first expression or the first expression inside the literal notation here, which is the 40. And this leads to an object being created, which has the number 40 in it. It's, of course, an int object. And now we have a reference from the first slot to the int object here. And then Python evaluates the 50. And maybe to illustrate a point, maybe the 50 is placed here because we don't know where Python is placing the stuff. Python is managing the memory for us. And so the second slot has a reference here. And now the right-hand side has been evaluated. And now the reference to the created list object is stored in a variable called simple. So the name simple is created and references the list. So this is what we have done in memory now. Okay, let's go on. Now let's create a third list here. And the third list is called nested. And here, this is also, of course, to illustrate a point. I create this list. And what I'm going to illustrate here is this list is not homogenous. So far, all the lists we have seen in this book are homogenous. So that means we either have text strings in them or numbers. But now I mix them. I mix the data types. Does this make sense? To be honest, this doesn't really make a lot of sense. Usually when you put data into a sequence, then what you put into a sequence is something that you want to be homogenous because usually what, when you work with sequence data, is you want to do all the operations one by one for every element in the sequence. And usually this requires that all the elements in the sequence are of the same data type. However, here, for illustration purposes of how the list type works, I made up this example to show you some of the underlying facts here. So first, what happens, of course, maybe I zoom out a little bit because this picture is going to become bigger. So let's go ahead, put there a second list or a third list already. It's, of course, a type list. And now the question is, what happens? So let's evaluate the first expression. The first expression is just the variable empty. So what Python will do, it will look up in the names of the area of the memory where does this reference to and now the reference is going to this list up here. So what Python will do is it will put the reference here to the empty list. And then for the second, third and fourth element what Python will do, Python will go ahead and, for example, create a 10, which is just type int and make the reference go here. And then it will create a 20.0. So let's make the box a little bit bigger because we have to store the decimals, right? This is, of course, a float. And the third slot will reference this object here. And then lastly, some even bigger box will be created. Let's put it here. And this is the text 30. And the fourth slot is going to reference this one here. And now the last expression will be evaluated and this is just a reference to the list simple. So we already see that a lot is going on in memory here. And the important thing to see here is that, and we knew this before, lists contain only references. So all the objects, even though in software like Python 2.0, for example, oftentimes the numbers are shown within the list. This is not the case. Inside the list, we only store the reference to the object. So, and if I now evaluate nested, I get a so-called text representation. A text representation means I should be able to copy-paste this here back into the code cell and get back a new list with the same value. So this would, if I copy-paste this back into a new code cell and execute the code cell, all of what I'm seeing here will be created somewhere else in memory as well, the entire data structure, so to say. So what are other ways? Oh, and I forgot, pardon me, I forgot the name. So, of course, after this list here has been created, we will have a name called nested also referencing this list here. So now we have seen how to create a list with the so-called literal notation with the brackets. We knew that before. And now we use the list constructor. So constructors, as we saw in chapter two, are built-ins, callables that we can call and we can pass in basically any object. And then the constructor tries to create a new object of a certain type. In this case, the list constructor tries to create a new object of type list. And I pass to it range. And we have seen in chapter four when we talked about looping, the range built in in Python creates an object in memory that allows us, in this case, to loop over the numbers one through 12. So the left index is included. The right index is not included. So the range object allows us to loop over the numbers one through 12 on a one-by-one basis without creating all the numbers in memory. So maybe just to illustrate a point for a moment, I will put here a new sheet of paper and I will go from the inside out. So let's maybe delete the list here and execute this and I get back a range object. So what's a range object? Well, a range object is an object that's maybe to the following. I create a range object with the name R. Okay, so what happens? Python creates an object and this is the range from one through 12 and it goes by the name R. This is all Python does and the range object is, of course, of type range. So only one object is created and now if I want to loop over this, I can do so but as we have seen in chapter 4, in every iteration of the loop of a for loop, for example, I only get back one integer at a time and I can do something within the iteration of the for loop, within the body of the for loop and then after the iteration is done and the next iteration begins, we don't store a reference to it. So the range object allows us to loop over a series of integer numbers in a way such that we don't need to have all the numbers existing in memory simultaneously. And now if I go ahead and say, or maybe just leave this here and create a new cell, let's say if I use the list constructor and give it R and execute a cell, I get back a list with the numbers from 1 to 12. So what now happens when I execute the second code cell, Python looks in memory, looks up the name R, sees that it's a range object and then let's check for a moment what the list constructor needs. So the list constructor, if we go into the documentation, we see the list constructor takes an iterable and returns a list. So we have seen in chapter 4 that a range object is iterable. We can loop over it. And so what the list constructor does, the list constructor says, well, okay, I go ahead and I allocate some slots here and then the list constructor goes to the range and says, give me your first number and then the range object bits out the int object with the number one, which is an int and then the list constructor puts a reference here and then the list constructor asks the range object, please give me your next number in line and then the range object bits out the two and then somewhere in memory the two will be stored the list gets the reference to the two and then the list constructor goes on and on and on. Let's do it like this until the last number is being given out which would be in this case the 12. So at some point I have the 12 and then maybe that should work. The reference, the last reference goes to the 12 and of course we have references and in all of those let's also do some to the top we have references in all the slots and of course we have objects everywhere right so maybe I do it like this so you understand what I mean so the difference is the object R so let's also give this a name it's maybe stored under the name L for list and execute it and now put the L here we create a new name L and make the reference go here so the difference is the range object knows how to create the numbers 1 to 12 it's a rule that knows how to create the numbers but it doesn't store the numbers 1 to 12 so the range object is very memory efficient all it does really it knows what number to start at what number to stop at and how to calculate the individual number so it's very lean so we pass the range object to the list constructor then what happens is the list constructor does something that I would call materialize all the objects so at the end of the day after the list constructor has consumed the range object we have 13 objects so we have one list object and we have 12 individual int objects and then they all exist in memory at the same time so once in 0s an energy basically in memory is being used to model 13 different objects and again so basically from a semantic point of view the range object models the numbers 1 through 12 and the list object models the numbers 1 through 12 but the list object needs way more space in memory and the range object doesn't and this is already a hint at what is to come in the second part of chapter 7 we will talk about many more objects that are kind of like the range object in that they model what I would call a rule in memory, a rule that knows how to calculate numbers or objects without calculating the objects that's the important part here I put back the memory diagram of the list because we will do some work with it let's go on here so we have to be careful here I can call the list constructor with a range and a very large integer so what this would do this would create the numbers from 0 so 0 1 2 3 and so on until this number here and it would create all of them at the same time in memory and then it would try to create a list that holds references to all these numbers and you see that's a whole bunch of numbers so if I execute the cell I hope that my laptop won't die so let's see what happens and I immediately get in memory error so Python luckily was smart enough to save me here and to basically tell me I cannot create all those integer objects because they wouldn't fit in my memory and that's what I mean by the range object is super memory efficient and a list object always materializes all the elements it holds and because of that we need lots more memory and sometimes we don't have the memory as we can see here and then if we are lucky Python may save us but maybe not maybe our machine dies who knows and then one last caveat about the list constructor we have seen in the documentation that the list constructor takes an iterable, it takes any iterable and the storyline in this chapter is that iterables is an abstract concept and the list or the range here they are just examples of an iterable and another example of an iterable is a text string so what happens if I pass a text string to the list constructor what happens is I get back a list with strings that each consists of exactly one character why? because if I loop over a string what do I do? well I loop over the individual characters as we have seen extensively in chapter 6 but also in the beginning of today's chapter so we must be careful here so now I do the first repetition so we have seen sequences, the four properties of sequences in the beginning of this chapter and now here I have an overview slide what are the four properties that make up a sequence? well they are the container property that means a sequence is any object that holds references to other objects just like the list does so the list here holds references to other objects that is the container property then we have the iterable property we can loop over the object so if I want to loop over the object what I do, I go over the references one by one and then my target variable in a for loop for example would be set to one of these objects here one at a time that's the iterable property and then iterable just means that I can loop over it, it does not mean that I can loop in order so the order comes from the third property which we call reversible and if you want to know if an object is reversible you always just use the reversed built in and then the fourth property is called sized and that just means that our sequence must have a finite number of elements and you see it here the number of references is finite here so that was just a wrap up and let's just see them in action and let's play you a little bit with these properties so at first I can pass nested for example to the LAN function and if I go into my memory diagram I see that nested totes exactly 5 references this is why I see the number 5 here so important important observation the nested list contains a reference among others to the list simple and the list simple has two elements the 50 and the 40 so we could argue that the nested variable here looks at more than 5 elements because we look at 5 references here but if you follow the references then some of the references will follow they will follow to more than one reference so we could argue that there are more than 5 elements available via the nested variable however it doesn't matter the only thing the LAN function looks at is the number of references that are in the object that we look at so only those 5 references constitute here the length of the object then comes the integral property so here we loop over nested one element at a time and within the body I have a bigger print command here and I pass several arguments to it and I also use the l chest here the l chest which is a string method which we saw in chapter 6 to make the output look a little bit nicer in a tabular fashion and this is to illustrate the point that I have what the list holds are here 5 objects and all of them have an identity that's what I do with the id function so all of the objects could live without the list but we know by now that they only can live in memory if they have a reference to it somehow but the reference does not have to come from the list of course so if somehow I can get a reference from the list of global names to the object here and then I could remove the reference from the list and then the object would survive so the important idea is whatever is in the list is also an object in Python everything is an object and then of course because they are objects they also have a type and here I just want to reiterate the point that lists could hold references to objects of different types even though it may not be useful but we can hold references to different types and we will see in chapter 9 when we talk about array and data frames we will see that there are data types that are similar to lists that can only hold homogenous data so lists can hold heterogeneous data but other more performant data types can only hold homogenous data and we will understand that in chapter 9 so we can go in backward order with the reverse built in let's quickly read the documentation on the reverse built in so that we also see again that some technical terminology is used here here it is so reversed and reversed takes one argument and the argument is called S-E-Q and by now after this chapter you should know that S-E-Q stands for sequence and the sequence is any object that fulfills these four properties that we have talked about that's why reversed works here and then of course we can check the container property we can ask is the number 10 in nested and it is because what now happens maybe to provide you some details here what does the in operator do so now what the in operator does it goes from left to right it follows all the references the first reference, the second reference and so on and compares whatever object it finds at the end of the reference with the 10 here with the integer 10 and it uses behind the scenes the double equal operator the comparison operator and therefore we can go over so let's do it we go from the left it follows the first element the first reference and this is the empty list so the empty list is not equal to 10 then it follows the second element and then it finds the in object 10 and the in object 10 of course is of course the value of the 10 is the same as the 10 that I'm checking against so it is about what I'm saying is it's about what is inside the box so to say and of course maybe let's also check the 20 here maybe let's check if 20 is also in nested and now we know that let's see what the answer is the answer is true let's see how the operator works so I check if the integer 20 is in nested however nested does not contain an integer with 20 it only contains a float with a 20 but we know by now that the float 20.0 compares equal to the integer 20 which is why we get back a true however the word contain in this regard is not 100% accurate because integer 20 is not contained in the list it's the float 20.0 but that's just a minor detail then of course the 30 is not inside so I get back a false so these are the four properties and now just as we did in chapter 6 because the list type fulfills those four properties we can expect some functionality to come with the list object so for example we can index into the list so for example I can index with zero to get the first element and in this case I get the empty list I could also try to get the second element by indexing with one now because list are finite I can index from the end so I can index with negative one I get back the 40 and the 50 well if I follow the last reference I follow the reference here to the simple list and the simple list contains two numbers so that's why I get back a number a list consisting of two numbers so I'm actually relying on two of the four properties here the first property I'm relying on is order because I couldn't index into an object if it was if its contents weren't ordered I couldn't index and the negative one only works because the object is finite otherwise if there is no end how could I get the last element this would also not work so these are two properties that are at work behind the scenes here and now because when I index with negative one to get the last element what I do I get the reference to the simple list but the simple list is a list again so of course I can also index the simple list and how do I do that I just chain two indexing operators so the first index, negative one gives me back a reference to the simple list and then I get the first element here the element index one which means I get really the second element which means I follow this reference here and then also I follow the second reference from this list which follows to the 50 so let's check and indeed I get back the 50 so when you work with nested data and nested data is quite typical if you get your data from a web API then often times you have to chain the indexing operator a generalization of indexing is slicing so let's get the middle three elements of the nested list so how do we do that we index from or we go from the index one to the index four and we learned in chapter six when we did the same for strings that the first index is included and the second index is not included so the first is the start number, the start argument and the second number here is the stop and the stop is not included and the nice thing is if both of these numbers are positive then the difference tells us how many elements so we will get back three elements and just center three elements here so these three references and what do we get back we get back another list object so what really happens here now is that what I just executed would create a new list object which I'm not drawing right now but it would create a new or it does create a new list object which holds three references to this 10 to this 20 and to this 30 but we will look at this at the underlying workings here in detail in just a moment and of course there is also the chance the possibility to use a third number in the indexing operator which then is the step size so what this does is it gives me every other element now I have, I get back the empty list the 20 and then the simple list and in between the elements are skipped so you can have any combination and the step size of 2 here is the step size of 2 could be used here for example let's say you have a list of numbers and the numbers represent measurements of something and you know that the list of numbers that represent measurements they are distributed by or they are ordered by random chance so for example you did a survey and this list of numbers is the response of all the participants in your survey to one question it's a numerical question or something and then what you could do is let's say you are given a lot of data and it's too much data to fit into memory you could use this syntax here to down sample the data for example but this only is good to do if whatever data you work with is you know ordered by random chance if there is any pattern any sorted pattern in the data set you shouldn't down sample this way and now comes something that may be a little bit tough however with a memory diagram such as you see on your right screen this should not be tough so what do I do here I take a slice on the right hand side from the beginning to the end so we saw this also in chapter 6 this is the so called full slice I take the full slice and what I get back I store in a variable called nested copy okay so what is nested copy it's just the same list or is it who knows so let's look at this in the detail so when I take a full slice of nested and I store it in nested copy and then after that I compare nested to nested copy this kind of should evaluate equal right because I mean if you take a copy the copy should have the same value as the original so let's check that and it's of course the case so let's check another operator the identity operator and let's check if taking the full slice means that I have another object or do I have the same list object and the answer is I have a new object so in other words what the cell above here the first code cell here what this does when executed is the following it creates a new list and now the diagram will become a little bit messy and so what I will do I will use another color now so that you can distinguish between the references what I get back is the first reference is a reference to the empty list so I get back a reference to the empty list here this here the second one is a reference to the 10 so this will be a reference to the 10 as well the next one will be a reference to the 20 so let's have a reference to the 20 here and then the fourth one will be a reference to 30 so let's do a reference here as well and then the fifth one to the to the simple list. So how do we do that? Let me see. So let's let's go this way. So this is basically what the first line of code did. And this is good news and bad news at the same time. So why is this good news? Well, it's good news because all that needed to be copied was the references. And I always tell you that whenever something affects only references, then it's a very fast operation. It's a nice operation, something we like. That is that is the reason why creating temporary variables in Python is very cheap operation because all it does it creates references. And we like that. However, what we don't like is that we have now a data structure in memory where it is not so obvious where something is pointing to. So now, you know, understanding and reasoning about the code is not so easy anymore, because you have more than one way of getting to an object. So all the objects that you see, or most of the objects you see, actually, all of the objects you see have at least all of the nested objects that you see have more than one reference or one more than one path to them. So this could mean trouble. And we have seen similar troubles in the first chapter already, although the situation here is totally different than the situation that we saw in the who am I and how many section in chapter one. So let's play, let's continue a little bit here and see how we can, you know, live with this situation in memory. So first of all, let's confirm this. The first element in nested is equal to or has the same identity when the first element in the nested copy. Oh, and of course, again, this always happens. I should nested copy, I should give a name. So of course, we have a name here, nested copy. But now the first element in nested copy is this list above here. And the first element in the original nested is also this element here and this object. And because both references go to the same object, this object only exists once in memory. So both references have the same identity, the same, they point to the same memory address here. So this is why the next cell is true. And that basically means that all of the nested objects are now shared between the two lists here. Now let's talk about a topic that is actually reasoned for why reasoning about this code will be rather tough, at least in the beginning. The topic is mutability. So we have seen an early example of mutability also with lists in the first chapter. There was exactly the who am I and how many section in the chapter one that I was just referring to. So what is mutability and its opposite immutability? So first of all, let's review what is immutability. In chapters five and six, when we looked at numbers and text data, we said that all the data types we talked about in these chapters, they are immutable. And we said that this means after, for example, a text string, oh, I forgot the string type here as well. So after a text string is created in memory, or once it is created in memory, we can never change what is inside the object. So all the ones and zeros that are collected inside this box can never be changed. The same holds true for floats, the same holds true for int, and these are all the data types in this diagram that are immutable. And that's actually a good thing because if that means this, even if there's more than one reference going to it, it will always be the same, no matter what, because we are not allowed to change it. And there are actually other programming languages that only have immutable objects because it makes reasoning about code easier. But in Python, we do have objects that are mutable, for example, the list type. So what that means is the once the box that is a list type, a list object has been created, I am allowed to change the ones and zeros inside it. That means in other words, all the references we see here, they may be changed. In other words, I can make the references go somewhere else. Let's do that. So first of all, I take the first, I look at the first index in the nested list and I change it to zero. I execute this, so what happens? So in the code, in the memory, Python follows the nested name to this list. And then it follows the first reference. And this reference is now to be changed. So what it really does, what Python really does here is it goes ahead. And for example, here, it creates a zero of type int. And now it wants to change the reference here. So first, it has to go ahead and remove this reference here. So this reference is now gone. And then Python will create a new reference that goes here. Maybe, because this is the new one, I make it in red. So this is now the current one. So this is what happened. So I can actually change the contents inside the object. That's what mutability means. Now I just changed a single object, a single element. And if I now look at nested, nested is now zero in the first element, which is, I mean, makes sense, right? Because that's what we just set it to. What I can now also do is, I can also assign not just to an index, but to a slice. So what I do here is, I assign to the first four elements here. So why is it four? Well, because I skipped the first, the start, which is zero. So four minus zero would be four. So this is a slice consisting of four elements. If you don't believe me, we just comment the rest out and execute this. So we have four elements here. And now I replace these four elements with a list containing three 100s. Now I execute this. And let's see what the nested list is now. The nested list looks now like this. It still seems to have five numbers in it, but we are already suspicious that maybe the length of nest will now be four. But we will see, we will check that. So let's try to put this in memory somewhere in our diagram. So what I did is, I changed the first four elements in nested. So these are these four. And what did I do? I made them point to objects with the, of int objects with the value 100. So what this means is an int object with 100 is created. And then another int object with 100 is created. And maybe one more int object with the number 100 in it is created. So three of them. And then Python goes ahead and removes this reference here because it's the first one. I want to now change the first four references in this list. So now the first reference gets removed. And what does that do? Well, the zero that I have here now has no further reference, right? And we always say that we assume that the garbage collector, once there are no more references, removes the object from the memory. So this object is now gone. And let's say, just to make a point, now this, now this first element here, let's say we take a reference and make it point to this 100. This is what we just did. And then Python goes ahead and changes the second element. So what does it do? It crosses out the existing reference. That means here is a 10. However, this 10 here still has a reference going from the other from the copied list, right? That means this 10 here survives. And now, for example, here, that means the second reference, for example, goes here. And then we do the third, just for the third element, we delete this reference, we remove it. However, there is another reference going to the 20.0, which means the 20.0 survives. And then we take the third element and make it reference this 100. So now we have replaced the first three. But now, wait a minute, I should not, I must not forget that I have, I used to have like four elements here and I replace it with three. So one must be missing. So what's going to happen is this reference going to the 30 will now also be removed because that's the fourth element. However, the 30 string here has a second reference to it, the green reference. Because of that, it survives. And now what happens is that the reference to this simple list that goes here, this is now moved here. So let me do this maybe. So this is now the reference moves here. It moves one to the left. This is what Python does when we assign to the slides. And now you understand what I mean by this may, the fact that lists are mutable may make reasoning about code a little bit harder because now we have to keep track of all the references. And this is not so trivial anymore. So you may wonder at this point, what is the advantage of having mutable objects? The advantage is that assume we are working with large amounts of data and they hardly fit into memory. Let's say you have a big matrix and it covers up 80% of your computer's memory. And let's say you want to change some of the numbers in the big matrix. Now how do you do that? If the matrix is the data type you model the matrix with is immutable, the only thing you can do is create a new matrix object where you copy most of the numbers and replace the numbers you want to replace because you're not allowed to change the matrix after it was created. However, in the story I just made up, I told you that this one matrix takes up 80% of your computer's memory. How can you make a copy of something that already occupies more than half of your computer's memory? Well, the answer is you can't. In this situation your computer would die. It would go out of memory and just die. And therefore you just couldn't do that. So let's say, in other words, in an environment where you want to work with big amounts of data, maybe it is absolutely reasonable to change something after it was created, to change an object after it was created. And what we also say is whenever we mutate an object, that's the technical term, we also say we change it in place. So whenever you read some Python programmer say or write, we change something in place. That means we basically change an object. After it was created, we change some of the ones and zeros inside the object. This is exactly what we did here. So there are reasons for allowing mutable objects in the language. Good reasons. And in particular, if you want to work with big data, that's important. And since this is a course that is going to prepare you for data science, at least in the coding part of it, you should already get familiar with some of the aspects of how to model a situation where maybe your memory in your machine or your server is kind of scarce. And that's what we need mutable types for. Okay. So now let's also look at nested copy. And now the interesting thing is nested copy is still the same. At least it looks the same. And now the question is, is nested copy still the same? The answer is pretty much yes. Because all the references are all the same. We didn't change the references of the copy. And all the objects to which the reference go, they are still there. We just keep them. We didn't delete them. And because of that, nested copy is still in the same state as it was before. Okay. Now let's continue to study here. I go to the nested copy and let's first comment something out here. What I do here is we go to the nested copy and we take, we look at the last element of nested copy. And this is of course a list, the list that contains the 40 and the 50. And this list also has another name. It goes by the name simple. So we're talking about the last element in nested copy, which is this reference here, which follows to this list up here, which contains the 40 here and the 50 here. And now what I do, I assign to the full slides the list 1, 2, 3. So what does that do? That's an example of basically removing all the contents in a list and replacing them by other objects. So what that means is after I, let me execute this cell. So what I do is I follow this reference to the simple list. And now I say I want to get rid of all the elements inside. So I will first basically get rid of the first reference. So the 40 only has one more reference left. So no more reference left. So the 40 is gone. And also the reference to the 50 will be replaced or will be removed. And then the object with the 50 in it is also gone. And now I need to find some more space to make up some more objects, namely the one, which is an int, the two, which is an int, and the number three, which is also an int. And let me see. Maybe I used green arrows here again, because there's already some red here. So maybe the green is more obvious. So the first reference will now go to the one. The second reference will go to the two. And the third reference will go to the three here. So now I have changed the simple list. And maybe, let me see. Now let's look at nested copy. And now the nested copy, we have, we see the original nested copy above. It used to have 40 and 50, a list with a 40 and 50, a list with two numbers in it. And now the fifth element in the nested copy is another list. Sorry, pardon me. It's the same list because we didn't change the list. It's the same list. But the contents of the list are different. And maybe just to also illustrate this point, if I go ahead and ask Python, what is the simple, what is the simple list? The simple list is basically following this reference here. And this is, of course, also at the same list with the numbers 1, 2, 3 in it now. So even if we never changed anything via the variable simple, when we now read the variable simple, all the contents are changed. And that's also something that makes reasoning about code hard. So whenever you have more than one reference to an object, you know, going through one of the references and making changes, will always result in the other name seeing the changes. And this may be confusing. But the thing is now, you know, when I started out, I did not have anyone that showed me these kind of memory diagrams. So I was confused back then. But now you have seen really what's going on. And the thing that you have to understand is whenever you have more than one reference to an object, which is mutable, then, you know, this kind of reasoning becomes hard. Whenever you have more than one reference to an object that is immutable, like an int or a string, then all is easy. Because no matter how many references to an object you have, if you cannot change it, you cannot do what I just did in the example. You know, you cannot just change references because you're not allowed to change the object. Okay. So let's go on. Nested. Let's look at nested. Nested is 100, 100, 100. And then the list 123. Let's confirm that with our memory diagram. I follow nested. And so the first reference goes to 100. The second one goes to this 100. The third reference goes to this 100. And then the fourth reference is here. And this goes to the simplest here, which now contains the 123. So in our memory diagram, we already see that the result makes sense. Okay. And now what we have done is we have created lists and we have copied lists and so on. And now let's continue changing some references. So for example, with the Dell statement, of course, Dell we have seen in chapter one, which and what did we do back then? What we did is we removed names from the global list of all names. And by removing a name from the global list of all names, we also removed the references. And usually in chapter one, at least this meant that we only had always one reference to an object and this always led to the object also being removed. But you remember that I told you that the Dell statement sounds like it deletes something, but the Dell statement does not really delete an object. All the Dell statement does it deletes the name, the variable. And only if there's only one, only if after the deletion of the name there is no more reference left to the object, the object is deleted. So this is why the term Dell statement may be a bit confusing. But now if I execute this, del nested index negative one, what happens is what Python does, it follows the reference to the nested list to the last element. The last element is now here, the fourth element, this one. And it executes the Dell statement. Now what does it mean? It does not mean that we delete this list up here. All it means is that we get rid of this reference. Okay? All the delete does, it removes this one arrow here, this one reference, that's all it does. And we can of course, and then what happens is nested is now only consisting of three numbers and all of them are 100. And this is true. These are the three red references here which go to the 100s. And then of course, we can also use Dell and use it, for example, with the full slice. And if I do that, what then happens is Python indeed goes to the nested list and removes all the references. So this reference gets removed. And because this 100 doesn't have no more references to it, this 100 is gone. And of course, the same happens to this 100. And lastly, to this 100. And now you cannot see anything anymore here in this area. However, you can believe me that now all the references are gone and we are left with the empty list. However, the one thing that stayed the same all the time was the list. So the list itself never changed its identity. It's still the same list as in the beginning. The list is only empty now. Okay. So I think if you go through the slides without what I just showed you here in memory, it will be kind of hard to follow. But with now this diagram and also with the fact that this diagram on video here is an interactive diagram, I think it's rather reasonable to follow what is going on in the chapter. Okay, let's look at another chapter, another section called list methods. So list methods, it's quite similar to string methods in chapter six. So we said that already in chapter one we observed that objects of different types, they come with different methods. And this is what we mean by saying that objects of different types have different behavior. So what are typical behaviors a list has? So let's start with an easy example, a new example. So we have a list called names and we put two names in it, Carl and Peter. And we create the list. So now what can we do with the name? So maybe let's go ahead and also draw a little diagram here. So what this does is, so this gets created and then a new here, a string gets created. And what I do now is I will just abbreviate the string with the first letter. So this is here is string, here we say list, the reference to the name Carl. And then here we have a second object which is Peter, which is also a string. And then after the right-hand side has been evaluated, on the left-hand side we will create a variable called names and make it reference the list. So let's go on. What can I do with the list? So now the next step will explain to you why Python already allocates many, many more slots to the list that are empty in the beginning, because now I want to make changes to the list. So now I call a method called append and I pass it a single object. So names dot append and I give it a German name Eckhard. And now observe something. This is a different to the string methods in chapter 6. Observe that we don't have an output here. There is no out. So what that means is whenever we don't see an out, the value that was returned is none. So none is not shown as output. And the rule is this, whenever you work with immutable data types, like the string type in chapter 6, and you call a method, for example, a string method called lower, then what happens is you get back a new string object with all the characters lowercase. But the important thing is the old string still exists and a new string with all lowercase letters is given back. That is why string methods in chapter 6 have a return value. Now, here in chapter 7, the list type as we saw is mutable. And the rule is this, whenever we work with methods on a mutable data type, usually we don't have a return value. The return value is none. Why? Because what happens is whatever the method does, it does it in place. So it mutates the list. So what this append does, it creates a new string, Eckhard, and references it. So this is what append does. It just creates a new object and moves it in. And this is now why Python starts with many empty slots here. And why does it do that? Well, obviously, once this array, this is what we call this structure, once this array is full, and we still want to append one more element to the list, what Python then does is it creates somewhere else in memory, a new list object which is twice as big as the old one, and copies over all the references. And that is a reason, and this is so because in other programming languages like C or C++, we as the programmer would have to decide how big the array is. So we as the programmer would have to manage the memory on our own. But I told you from the inter part on that Python is a high-level language that abstracts away many of the low-level things that go on in the computer from us. And the thing that Python abstracts away the most from us is how the memory works. And because Python does not know how many elements we are going to put in the list, Python has to make some assumptions. And the assumption that the core developers in Python made was we rather waste some memory and give you a list that is too big, because then what you can do with it, you can still put some stuff in it, you can still mutate the list, and there is a very high chance that there is always enough space in memory. So again, because we don't manage the memory on our own manually, this is why Python makes such assumptions here and allocates too much space, that's just an optimization so that in most use cases the code runs fast. Okay, let's look at some other list method. So first of all, let's look at names again, and now we see that names is a list with Karl, Peter and Eckhardt, but we can see that too in the memory diagram. And now there's a method that is quite similar to the append method, which is called the extend method. And the only thing that is different is the extend method also takes one object, but the one object it takes has to be an iterable, and a list object is of course an interval as we saw. So the object that we pass in as the argument is a list itself, and then of course the iterable can have more than one element. So this is why I pass in one list object with two elements, and it two names, Karl and Oliver, to the extend method. And then what happens is the extend method loops over the iterable that it was given, and it pulls out the elements one by one and appends them to the list. So what that means here is Python goes ahead and creates a new box here for Karl, and Karl goes here, and then also creates a new box for Oliver, and the box goes here, or the reference goes here. So that's it. And now let's look at names again, and of course it is what we expected to. And again just to emphasize the names.extend method call does not have a return value, so the return value here is none. And the reason for that is because names was changed in place, and returning none from a method usually indicates that the changes you made are happening in place. So we have seen append and extend, and now there is of course some other method that is similar. It's called insert, and now what does insert do? Well, insert gives us the chance to put in a new object in the list at any arbitrary position. Of course, we cannot skip, we cannot have empty slots, that's the only rule. So what we do here is Bertolt will be inserted at the position index one, which means at the second position. So let's see how can we do this. So let's first create a new object Bertolt, and now we need to make space. So let me do this. So what happens is the reference to Oliver is moved, the reference to Carl is moved, the reference to Eckhardt is moved, and then lastly, this reference here is also moved, and then the free space, the free slot now, is then used to put in Bertolt. So again, we move all the arrows one to the right to make space, and then we put in another reference to Bertolt. So this is why now Bertolt is in the second position. So now comes something that you just have to learn. Now I have a list of names that is unsorted, and I want to sort it, so I use what? I use the sorted built-in function. So let's, before we look at the documentation, let's just use it here. I call sorted, and I pass it names, and I get back the names in alphabetical order. This should already make you suspicious. Why should this, why should Python know that we want to order it in alphabetical order? There may be other ways to sort the list, but I mean, at least somewhat intuitively, the list is now sorted alphabetically. But now, this looks different, doesn't it? So far in this chapter, or means in this section, we only could use methods, and methods are called with the dot operator here. So it's all methods, right? So it's always names.something. And now all of a sudden, I call a built-in function called sorted, and pass names to it. So we better look at names. So what, what is names? And names are still in the old order. So we still have Carl in the first position, and Bertolt in the second, and so on. So names didn't change. So this is also something you have to learn. When we have a mutable data type, and we call a method on a corresponding object, then usually, as we saw, the object gets changed in place, and we get returned none, and that is what the method does. But whenever we pass our mutable object to some other function, so we don't call a method, but we pass it to some other function like a built-in function, then chances are pretty high that the object does not get changed in place, but a new object is going to be created. And this is exactly what happens here. So let's quickly look at the sort function, the sort built-in function. Here it is, sorted. Let's read the first couple of lines. So sorted, first of all, it takes one argument called iterable. So again, this is independent of the list. We can pass in any data type that is iterable, for example, also a text string. But in this example, we now are using a list. And then we already see there are two more arguments, keyword-only arguments. That's what the star means. We also learned about this. And one argument goes by the name key, and the other by the name reverse. We'll soon look at what these two arguments do. And then the first line says return a new sorted list from the items in iterable. And item is just another word for elements. Elements is the more common word, but item is also fine. So basically, what this basically tells us is that it takes an interval and creates a new list. So let me quickly draw in memory what this did. What this did is this. It created a new list, list type. And now let's use the red here. And the first name is bad hold. So it goes here. The second name, we have bad hold twice. That's interesting. So probably I have executed a cell more than once, but that's okay. We can handle that. So that means here I have, let me see, one, two, three, four, five, six, seven objects. And here I have one, two, three, four, five, six. So obviously, the cell that inserted bad hold, I must have accidentally, I must have accidentally, let me go back. Yes, it's not in there. So I must have accidentally executed twice, but it's okay. We can also, as we will soon see, remove a bad hold again. So now let's pretend bad hold is in there only once. So we have bad hold. The second reference goes to Carl. And then the next reference goes to Eckhardt. And then the next reference goes to Carl with a K. And then the next reference goes to Oliver. And the last reference goes to Peter. So this is what happened. This is what the sorted built-in function does. It creates a new list out of an interval. And the interval just happens to be another list. Okay? So this does not have to be a list. But definitely the sorted built-in function will give us back a list. And it will rearrange the references such that the resulting list is sorted. But we get a new list. That's the important thing. And then what we see here, the sorted build-in function has an output. So this is definitely different from a method. And what this means is the reference to this new list is given back. And in this situation, I don't store the new list in a variable. So technically, because I don't store reference to this list, this list would immediately be forgotten. But I would just keep it on the diagram so that you can see how the structure looks like. So now the question is, how can we make sure or how can we make Python rearrange the references up here instead of creating a new list with changed references? I want to change the references up here to point to the objects in the correct order, in the alphabetical order. Well, obviously, I have to use a method for that. And the method is somewhat unsurprisingly called sort. So names.sort will be the method that sorts the elements in place. Let's call it. And then let's see names again. And now, indeed, names has changed. So from names from here to here, now names was changed in place. So now all of these references were changed in place. Okay. So I think I will, for now, stop updating the memory diagram because the two or three more methods that I need to come are rather trivial to understand. You get the point. We have added new references. We have now changed the order of references. And the missing part that I will show you in the next slide will be removing individual references from the list with methods. So let's see how we do that. Oh, okay. We still have one more slide to go on sorting. So this was sorting with the alphabet. And the reason why the alphabetical order was chosen is because whenever we sort a list, the list doesn't know anything about its elements. So the list basically goes ahead and asks the elements to sort themselves. And then because in this example, we have only strings in the list, the strings sort themselves just as we learned in chapter 6 on text. And then by default, that means they will sort themselves according to the unicode code point. Right? So that means uppercase letters will always come before lowercase letters. You know, you remember the example in chapter 6 on apples and bananas. And if we write apple in the lowercase a and bananas with an uppercase b, then the banana will come alphabetically before apple, which is weird. But all the uppercase letters come first. So that's how sorting goes. The list delegates the sorting to the elements of the list. It does not know how to sort itself. And if I want to sort in reverse order, what I can do is I can call the sort method with the reverse argument here, the keyword argument. I have to use it as a keyword argument. And then I have, again, no return value because it's a method that does something in place. And now if I look at names again, then names is sorted in reverse order. So Bertolt, which used to be first, is now the last. And then also, if I want to generate a different ordering or sorting, I can use what is called a key function. And let's assume I want to sort the names by their length. So for example, the both cars, they only have four characters in their name. And Bertolt seems to be the person with the longest name. So I want to sort the list such that the people with the shorter names come first and the people with the longer names come last. So what do I do? Well, what I have to do is I have to pass in a reference to a function that is then executed for every element in the list. And then whatever the function returns is sorted. And that's called a sort key. So for example, I could call Len, of course, with Peter in it. This will give me back five. And Oliver would be six. And then that means Peter comes before Oliver because five is smaller than six. So note that I don't have parentheses here. So I just passed a reference to the function, which is the variable name Len and not the call operator. And then what happens is the sort method goes over all the elements in the list and calls Len once for every element and passes in the name as the argument. And whatever number is returned, they are sorted. And then according to this ordering, the names will be returned. So let's execute this and see what names look like. And now we see that the names are sorted in a different way. And note also that Carl with a K comes before Carl with a C for one reason and one reason only. And that is in the state that the names list was in before Carl was before Carl with a K was before Carl with a C. So there is no type breaking rule. The type breaking rule is if two objects would go in the same position by sorting, then whatever object was first before will also be first after that. And we call that property of a sorting algorithm. We call that a stable sort algorithm that does not change the ordering in case of a tie. And then we must not confuse the sort method with the reverse argument, which does reverse sorting with the reverse method. So the reverse method just goes ahead and reverses the list just as it was. So now Bertolt will be first and Carl with a K will be last. And this is just we get. So reverse just reverses all the references. Okay. And now we come to the topic of getting references out of it. There are several ways. Most notably, of course, is the pop method. The pop method on the list, what it does is it looks at the list and it pops off the last element in it. And now this is an example of a list method that actually has a return value. And here in the example, I capture the return value in a variable called removed. So let's look at what was the list before. So the current state of the list is we have Bertolt first and Carl last. So let's pop off Carl. And now removed is Carl. And the names list is the list as before, just without Carl. So Carl is gone, but we had a chance to capture it. Sometimes you don't need this. If you don't need Carl, you just do it like this. You don't capture it. But you could capture it if you want. But definitely the names list was changed in place as well. So Carl with a K is no missing. And of course, the pop method takes an argument. So if we don't pass in an argument by default, the last element is popped off. And if we pass in an argument, it's of course the index. So here index one, which will get rid of this Bertolt here. So let's do it. Now remove this Bertolt and names is Bertolt Eckhardt and so on. And now, because I just got rid of the second Bertolt, I accidentally included here, I would just run this cell again to correct my mistake from earlier. So now the list is now Bertolt, Oliver, Peter and Carl. And yeah, let's go on. So popping means we pop off, we remove an element by index, but we can also remove an element by value. So in this case, the names list is asked with the remove method to remove any object or the first, let me, that's important, to remove the first element that evaluates equal to Peter. And because there is only one Peter in the list, the one Peter is now gone. So now Peter is gone. And if I were to execute this again, we can actually do it, then I will get an arrow. So Peter is not in the list. And because we removed it before, and if it's not in the list, the remove method will raise a value arrow and tell us, okay, I couldn't remove what you wanted me to remove. So with the remove method, we have to be careful if there is more than one element in the list that has the same value, then only the first gets removed. So that's the state of names. And then these were the, so the methods we now had was, were about removing references. And now we look at the group of methods that regard like, you know, asking the names list about where is something in you. So for example, we ask the names list at which position is Oliver, and Oliver is at the first position. So index zero, index one, index two, so Oliver is here. And if I try to look something up that is not in the list, I get a value arrow. And remember, in chapter six, the string methods, we had a method called find, a string method called find. And this method would return a negative one whenever it couldn't find something in the string. And the index method here, we actually raises an arrow. So this is an example of a loud failure. And the find method in this, on the string is an example of a silent arrow. And I personally prefer methods that have a loud failure if they fail, because then I just know that something went wrong in the code. And then, of course, we, in this case, we can, you know, count how often the skull appear in the list. And it's, of course, only once. So these were list methods. And now comes the next section on list operations. So what are list operations? Well, we know from the first chapter on what are operators. So for example, the plus operator or the multiplication operator. And when we first saw them in chapter one, we used them to do arithmetic, to do math. And now the list, the operators, some of the operators, they are what is called overloaded in the context of lists. So let's look at an example. I still have my names list. And I can, for example, add the names list to another list. And then I get back one list that contains all the names. So I can do arithmetic, so to say, with lists. And I can also multiply a list. So two times names gives me just a bigger list that has the first three names. And then the names repeat themselves in the same pattern. And this also worked for strings in chapter six, right? Plus and multiplication. So many operators are overloaded many, many times. Okay. And now we find some, we look at something that is not actually an operator, but it looks like an operator, namely the asterisks. And what is this? So here we write out a new list with a name Achim and it ends with a name Xavier. And in between I write names, but I, but I prepend it with a star. And what that does is it unpacks the names list. So just to illustrate, if I don't put the asterisks here, what I get back is a list with, which has Achim and Xavier in it, but also a list with the three names. So, but so in other words, I have a list that contains a list. But if I use the asterisks operator, I unpack the names list and unpacking means the elements within the names list, they are put into the new list in the same layer as the elements in the, in the outer list. Okay. And this is the same as if I wrote this out. So it's the same as, you know, creating a list like this. However, the second code line, it only works if we have a short list. And this does not scale. And the asterisks operator can unpack as many elements as we want. And this is nice. So the asterisks operator for unpacking is used quite often when, when we write programs. Okay, so how does this comparison work? This comparison comparison is of course also an operator comparison works with the double equal operator, which is the comparison operator. So here we have names. And let's compare it to another list, which happens to be also Bertolt, Oliver and Carl. And of course, it's both lists on the left hand and the right hand side contain the same strings. We get back a true. So how does list list comparison work? Well, it works just like string comparison. In that we go, we compare both lists on the left and the right hand side in an element wise fashion. So Bertolt gets compared to Bertolt, and they evaluate equal. And then Oliver gets evaluated gets compared to Oliver, and they compare equal. And then Carl gets compared to Carl, and they also compare equal. And if both sides compare equal for all the elements, then the lists are evaluated true, or they compare equal. And of course, if I change one name, for example, here, Carl, not it was a C, but a K, then they are not equal. So they compare unequal here. And of course, I can also use smaller than this also works. And yeah, that's it. So all the relational operators would work with a list. And just if if one of the two lists is shorter than the other one, but the ones, but all the elements, they have still in common, then the shorter list would go first. And the longer list is always the second one, the crater list, so to say. So but basically the rules of comparison work just like they work for string comparison. And yeah, so you see the point basically, right? The big storyline of this chapter seven is that we talk about sequences and in abstract terms. And list is just one example of a sequence. And another example is a string. And we see that in many, many ways, strings and lists, they behave alike. And in terms of duck typing, we would say they walk and quack alike. This is the term that Python programmers use. And yeah, so it's you don't learn a whole lot in this chapter, actually, because it's just repetition of chapter six. Now let's look at a nice example of something that will confuse some of you in the beginning. But after you understand it, once it shouldn't. So let's look at this example, I give you a new example now, a list letters with which contains the characters or the letters ABC as a text string. And now I define a function, which is called at x, y, c. And it takes one argument arc. And all it does it calls arc dot extend and extends the letters x, y, z. And then it returns arc. Okay. So let me let's just define it and let's run it. So on the right hand side, I call at x, y, z with letters, and I store the result to letters with x, y, z. That makes sense, right? We have letters list without x, y, z. I have a function that only adds x, y, z. And afterwards I have a new variable that is called letters with x, y, z. And let's just do it. Now most of you would probably say this letters with x, y, z should be a list that contains ABC and x, y, z. And so it does. That is correct. And then most of you would say the list letters should only contain the letters ABC. And here you are all wrong because letters is also ABC, x, y, z. Now, what's going wrong here? Well, I could, of course, draw this here on a memory diagram again, but just to illustrate how you could do that on your own without doing the memory diagrams, let's go to Python tutor. And I copy pasted the code here. And let's just walk through it. It's just seven steps. Let's just see what happens. And then we will see what goes wrong. So first I create a list called letters. And again, here, Python tutor tends to write strings and also numbers inside the list. But we know that this is not the case in Python. In Python, a list of strings will always have references to real string objects. So this is a simpler, a more simplistic way of showing it. Then the next step defines the function. So we have now a function object in memory. And we have a name at x, y, z that references it. And now we get to the last line. So what happens here? The right hand side gets evaluated first. And then that means the flow of execution goes to the function. So that means a function scope is created. And now we see exactly what the problem is. Now the function takes a parameter arc. And when the function call occurs, the function scope is created. The name arc is inside. And what does it reference? Well, it references the list outside, right? So now the list ABC has two references, one from letters and one from arc. Let's go on. And now we execute next line of code, which is arc.extent, x, y, z. So that means whatever arc is pointing to, which is the list out here, we extend to that list x, y, z. We do that. And then we have x, y, z here. And then we return the arc, return arc here. And that means the function now closes. So now what happens is arc is still referencing the list. The arc will be deleted. When once the function is done, the scope will go away. So the name arc will be gone. And then we have the return value, because it says return arc. And the reference of the return value will be stored in a variable called letters with x, y, z. So let's do that. Now we have letters with x, y, z referencing the same list object. Okay, that's the problem. And this is what happened. And the learning will be, we will just look at the correct way of doing it next. But the learning will be whenever you write functions, you should check what kind of parameter they take. And as long as your functions only take immutable parameters like ints or strings, nothing can go wrong. However, whenever your functions take a mutable object such as a list, sometimes something goes wrong. It depends on what you code, but then there is the potential of something going wrong. So now let's quickly go back into the slides and see how we can repair that. So let's do it again. So I set letters again to abc. And now I create another version of add x, y, z. It also takes one parameter called arc. And now I inserted one line of code here. And what we do here, let's get rid of the comment, I take a full slice here, I take a full slice of arc, and I assign that to new arc. And then new arc, I extend. So in other words, I make a copy. And then I only work with the copy. And oftentimes, this is the simplest solution that you can have to a function that processes a mutable argument. And then in the last step, I return arc again, the new arc. There is a third way in the chapter. So when you read through this, you will see now, this is the second version, you will also see a third version. But I skip that here because there will not be too much to be learned. So now I call add x, y, z again with letters. I store the result to letters with x, y, z. And now letters with x, y, z has x, y, z with it, but letters is still abc. Let's quickly look at Python tutor. What happens here? So first step, we create a list abc. Then we create the function. In the third step, we call the function. The function gets its local scope. It has a name called arc. The name references the same list object outside as letters. And then what do we do? The first line of code takes a copy. So here we have the copy. We have new arc referencing the copy. Then we extend the copy. And then we return, as we see, the return value will be the copy. And then we have two different list objects. So this is how the code is supposed to work here. Okay. So we have to be careful when working with multiple data types. So what else do I have? So that's it on the list type. That's what we... That's a lot of information, but this is what you should know about the list data type. And it's really worthwhile because the list data type is one of the most versatile data types that you will have in Python. And especially when you are learning, the list data type will basically be part of most of the solutions to most of the beginner's problems you will have. And as you get more advanced, you will use different data types. But in the beginning, as I said, the list data type is usually how you want to model your problems. Okay. So let's look at another data type, the tuple type. We have not seen this before, at least not officially. But we have actually used the data type before a couple of times without knowing it. And also note that the tuple type is also in this chapter seven of sequential data. So what is a tuple? A tuple. First of all, how do we create a tuple? Well, we create it just as if we created a list, but we just leave out the brackets. Okay. So let's execute this. And now numbers is a tuple. And you see that numbers is given out with parentheses. And this is the literal notation to which Python defaults. Note, I don't have to use the parentheses here, but I could. So let's let me add parentheses here instead of brackets. And now I can create a tuple. So there are two ways with parentheses or without. So now, just to emphasize this, the parentheses can now be used at, let me think, four different syntactically different situations. So the first time we saw the parentheses was, I think with the print hello world example. In this situation, it was called, it was used as the call operator to call a function. Then going along with the function calling is also function definition. So when we define a function, we also use the parentheses to define the parameters. That was the second application. Then in the chapter one, when we did arithmetic, we use the parentheses to group the group sub expressions. So when you have, when normally the multiplication and division goes before addition and subtraction, and if you have an expression where you want it the other way around, you would use the parentheses as a grouping operator. And now here, the fourth application of the parentheses is to create a tuple. So what are tuples? Well, there are of course full-fledged objects and the type of number series just tuple. So this is how we create tuples as a literal. We can of course also use the built-in constructor called tuple and the built-in constructor to show it to you in the documentation. Where is it? Here, it also takes an iterable. And that means I can pass it a list. So a list with just the number one will give me a tuple with just one element. And note, the comma is necessary. So I have here the so-called literal notation. This is what is the default output. So let's say I make a new cell and I copy paste back the value and then execute this. That's the tuple. That's the tuple that has one element in it, which is one. So if I go ahead and delete the comma, I get back one, the integer. Because now without the comma, the parentheses are seen as the grouping operator. Okay, so much for the tuple constructor. And then of course, as we just saw, the tuple constructor takes an iterable. So I give it a string. And because strings are iterable themselves, I create a tuple whose elements are characters or strings which only are of length one, which are individual characters. So we have to be careful here. I have the same example in the list part as well. So what are tuples? Well, luckily, we don't have to spend too much time on tuples here. Why? Because tuples are just like lists, only that they are immutable. That's the only difference. So if I go back to this complicated example here, this example was mainly complicated because we were able to change all the references. We could delete references. We could add new references. We could change the order of references and so on. That's what made reasoning about this problem here a bit hard. Also in the example that we saw just moments ago, where we passed a list to a function, we may end up accidentally change a list in the global namespace from within a function. That's also a danger. And all these downsides cannot occur if we use tuples instead of lists. Because if we do that, then we have all the capabilities that lists have just that they are immutable. So let's see what that means. We can of course ask how many numbers or how many elements are in the numbers tuple? It's still 12. We can loop over the numbers tuple so it's an iterable. So above here because we can call the length function means the tuple is a sized data type. Here because we can loop over it, it's an iterable. We can loop over it in reverse order so it's reversible. And lastly we can check if a number is contained in it so it's also a container. And because it fulfills all the four criteria, it's also a sequence of course. A sequence means we can index into it to get the first element. We can also get let's say the fourth element with the index three. We can take a slice of course. Must not forget we can also slide index of course with negative numbers also works. We can take a slice. And now this is the one thing that does not work. I cannot change a number. So here I try to change the last element in the numbers tuple. I want to set it to 99 and if I try to do that I get a type error and it says we cannot assign to the tuple. And why not because the tuple is immutable and in order to assign to a tuple we would have to change a reference in it and we can't. So just also to maybe illustrate how a tuple looks like in memory I will just put up this slide here on this diagram. A tuple looks just like just like a list right. So a tuple would basically look like this. We have slots again and here in this part it just says tuple and all the references that are collected in a tuple may not be changed. That's the only difference literally. So you don't have to learn anything new. So with Paco where have we seen tuples? Well we have used them implicitly whenever we do packing and unpacking and let me see let me show you what that is. So on the right hand side this is my numbers tuple which contains 12 numbers and now I assign the right hand side to a total of 12 variables and this may look weird right but it works and this is called unpacking again. So now after executing this N1 is of course 7 and this is similar than the unpacking that we saw when I passed in a list into another list and or when I to be more precise when I when I passed in the elements of one list into another list with the unpacking operator the asterix if you remember and this is a same similar behavior here I have the tuple on the right hand side it could also be a list by the way and I assign the individual elements to the left hand side however if you do that this has to be it has the the number of variables on the left hand side must match the number of elements in the tuple so if I add a 13th variable here I get a value error I have not enough values to unpack in the same way if I take away one of the variables here I have too many values to unpack in other words the number of variables when I do unpacking must be exactly matching the number of elements in the tuple and then this works and then here in two and three and so on and the ends all take the values of an individual element and if I don't know how many elements I have I can then use the asterix on the left hand side so in the previous example when we use the asterix to unpack a list we used it to unpack the list now what what happens here we that's basically the opposite it's basically called packing so what happens is I want to unpack numbers and I want to get it into these three variables and what what how does it work well the first element in numbers goes into first the last element in numbers goes into last and the middle 10 elements in the numbers tuple will be packed together in the middle variable okay so first is now seven and then middle is a list unfortunately it's not a tuple it's a list that is so because this is what the syntax says but it contains the 10 numbers in the middle of numbers and then last of course is the last number so this is packing and unpacking this is a nice syntax if you get data from one source that is stored in one variable and you want to like you know I don't know get the first 100 elements out and throw the rest away or something this is very valuable to know if you wrangle with with data and one use case of that is we can easily swap variables I think this is almost the last example in this presentation so bear with me so let's assume we have two variables a and b and we set them to zero and one and now I want to swap them so I want that b becomes zero and a becomes one how can I do that well I have to use a temporary variable otherwise it doesn't work so I set the temporary variable to a then I set a to b and then I set b to to temp and then ideally I would also go ahead and delete the temporary variable because I don't really need it I don't want it actually and then now a and b is now swapped and now I want to do that in a nicer way how can I do that well first if I want to define more than one variable I can always do that I can write it this so on the left hand side on the right hand side here the zero comma one is the same as parentheses around the same as having parentheses around so this is just the tuple the tuple syntax on the right hand side which creates a new tuple and the tuple is immediately unpacked into the variables on the left hand side and into a and b so if I do that then now a and b has the values zero and one and if I want to swap the variables this is how I do it I just swapped them on the right hand side and I swapped them on the left hand side and the right hand side will dynamically create a new tuple temporarily and the tuple has the opposite order and then it will just be unpacked into the left hand side let's do that and then of course a and b is swapped so you may wonder why would I want to do that well remember in chapter 4 our last version of the Fibonacci function yeah Fibonacci is coming again so here is an updated version of the Fibonacci function we still have type checking and input validation and then if you remember correctly the first two numbers of Fibonacci they are by they are zero and one by definition so I just define a and b like that and then I loop for all the numbers for i minus one I loop i minus one times and every time my next so that's the first Fibonacci number the one is the second in that so in other words a is supposed is whatever b was in the loop and then my next b is whatever a was plus b so a plus b is always the two numbers that add up to the next Fibonacci number if you remember what the Fibonacci number is how they work and so in other words instead of using three lines of code here and two lines of code up here I can use one and one line of code so the tuple unpacking and packing syntax allows us to write the code even more concise and this function is of course correct it does the same as before and we're not even checking it because we know it's correct okay regarding the unpacking and packing there is one more section and I'll remember so we can use the packing so the asterisk operator within the definition of parameters in a function so what does that do we have seen something similar like this we saw an asterisk in the definition of a function when we specified so-called keyword only arguments so arguments that we may only pass in with an explicit keyword and must not pass in it by position but now I have an asterisk followed by a variable name and let's before I read to you the code let's just see what the code does I define the function and now I want to calculate the product of just one number and what is the product of just one number it's the number itself and now I want to calculate the product of three numbers two five and ten what is the product of them so this is two times five which is ten and ten times ten would be 100 so this gives me back 100 and the the cool thing about this function is I can now pass in as many numbers as I want so as many arguments as I want I can just pass in and it works and why why can I pass in as many arguments as I want well because of this packing packing asterisk here so what happens is when the function product is called the star operator here the packing operator collects all the arguments that I pass to the function by position and collects them in a tuple so arcs within the function will be a tuple so let's maybe prove that so I just write in the print type arcs and let's execute the function again and the print causes the class tuple to be printed out so we can be sure that the arcs argument here is indeed a tuple so this is why this is one of the reasons why we may use tuples anyway implicitly without knowing it because tuples occur naturally in python for example when we pass in the arguments to a function that is defined with the star here which means we can pass in as many arguments as we want and it kind of makes sense because the star often means this you can pass in zero or more any number of arguments you can pass in that's what the star really means here and how did how is the function implemented just right just quickly we just go ahead and take the first argument in arcs so in this case the 42 in this case this two we set it to result and then we loop over the remaining arguments so arc from going from one to the end so we skip the first one because the first one is already set to result and then we update the result by with times equals arc so we calculate so to say the running product again that we have seen before and that is it so there is one exercise in the exercise sets that will make you look closely at this function because in the exercise set what you will see is this function works for one argument it works for any number of argument however it does not work for zero arguments if i call this function without any arguments i get an index error why do i get an index error well very easy because i try to index into arcs into a tuple with which has no arc which has no elements in it so i try to get the first element in a tuple that has no elements and this will definitely result in an index error this is why we see that and in the homework what you will do is you will repair this so you will you will start in the exercise with this function and you will enhance it and make it work in even more situations and by doing so you will familiarize yourself with packing and unpacking because i think it's quite valuable it if you understand packing and unpacking it saves you from writing so much code that you really should learn about this so this is the last slide in the presentation so this is part one in chapter seven and chapter seven as a whole i give you a quick summary is on sequential data so we looked at primarily the list data type but we saw that in abstract terms sequences are any objects or any data type in python this will fill four different properties and they are we can loop over the over the over the object which means it's integral we can loop over in order which means it is reversible because if there is a reverse order there must be a forward order to begin with all the sequences are finite we call that sized in in technical terms and what else do we have all the the container property so all the sequences they contain they contain are any objects that are contained that are containing other objects and we looked at the list data type in in detail but we also know that the string data type from chapter six is also a very good example of a sequence and the primary difference between the string data type in chapter six and here the list data type in chapter seven is that the string data type is immutable and the list data type is mutable but whenever we need a list that is not mutable we can always just use a a tuple also regarding tuples i just saw i showed you the normal version the basic version of a tuple here in the presentation tuples they have a second meaning so one common meaning is they are used as lists whenever the list should be immutable however there is a second use case whenever we want to model something that we would call a data record and in that regard we we can see them as a collection of values so for example a student has the property that a student has a name a first name a last name maybe a gender maybe an age and so on and another student has all the same properties but with different values and we could model students in a class by making every every student a tuple that consists of values that make up the names and the age and the gender and so on and however whenever we want to use tuples in this second way as a record as a data record of something then there is another data type in python which is called the named tuple and the name tuple i have a very short section in this chapter as well and this is also something worthwhile studying but you should first focus of course on the list type and because that's the harder part and the one thing that is common to all the data types in chapter seven is in chapter seven here or in part one in part one of chapter seven they all lead to many many objects existing simultaneously in memory and when we now get to part two of chapter seven we will look at data types that can model sequences without modeling all the data in memory simultaneously so when we are working with big amounts of data what you learned here in part one of chapter seven is okay to start with but for big data it may break down but then what you will learn in part two of chapter seven will enable you to write programs that run also for big amounts of data okay but conceptually no matter what data types you look at the in part one here or in the next part two of chapter seven they are both sequences so they're not so different okay so this concludes chapter seven part one and i see you in part two