[CLASSICAL MUSIC] DAVID J. MALAN: All right, this is CS50's Introduction to Programming with Python. My name is David Malan, and this is our week on object-oriented programming, or OOP. It turns out that, in the world of programming, there's different paradigms of programming languages. There's different ways of solving problems with code, and it's a little hard to see this at first if you've only learned one language. But over time, if and when you learn other languages besides Python, you'll start to notice certain patterns and certain capabilities of some languages but not another. Thus far, within the world of Python, you and I have largely been writing code that's procedural in nature, whereby we're writing procedures; we're writing functions; and we're doing things top to bottom. Everything is step by step by step, as you would expect in general from an algorithm. But along the way, we've actually dabbled in another paradigm known as functional programming with Python whereby we've been able to pass functions around. We even had an anonymous function some weeks ago. And that's evidence of features of a functional programming language, even though we've just scratched the surface thereof. Today we focus on another paradigm, and this one in more detail-- namely object-oriented programming. And now, while some of you might have prior programming experience and have learned languages like Java, which are, by design, fundamentally object-oriented, Python indeed allows you a bit of flexibility when it comes to how you solve problems with code. But it turns out object-oriented programming is a pretty compelling solution to problems that you invariably encounter as your programs get longer, larger, and more complicated. So indeed, OOP, for our purposes, is going to be a solution to a problem that builds on so many of the lessons past. So let's go ahead and do this. Let's start by writing a program very procedurally by opening up VS Code here. I'm going to go ahead and create a program called student.py. And in this program, I want to do something relatively simple initially, as we might have done some weeks ago now, where I just ask a user for their name and, maybe in the context of the Harry Potter universe, their house, and just print out where that student is from. And let's gradually enhance this program by adding more and more features to it and see if we don't stumble upon problems that, up until now, we might not have had very elegant, well-designed solutions to. But if we introduce, explicitly, object-oriented programming as a programming technique, I bet we can clean up our code and set the stage for writing even more sophisticated programs, longer programs down the line. So in student.py, let me go ahead and do a name variable, setting it equal to the return value of input, and just prompt the user for their name like this. And then let me go ahead and do the same for a house variable and prompt the user for their house, using input like this. And let's do something super simple now. Let's just go ahead and print out an f string that says something like name from house, just so that I can confirm that the contents of these variables are indeed as I expect. I'm not going to do any error checking or trimming or anything like that for now. I'm really just going to spit back out whatever the user just typed in. All right, let me go ahead and run Python of student.py. Let's use our go-to, like Harry, as in Harry Potter, from Gryffindor. And when I hit Enter, now let's see if I see that Harry from Gryffindor is indeed the case. All right, so I think we have a working program at this point, but let's now introduce some of those lessons learned way back from week zero where we started writing our own functions, not necessarily because it solves the problem more correctly-- I daresay this is correct as is. But it begins to give us building blocks that we can extend so as to solve more complicated programs. So let me go back up to student.py, and let's go ahead now and do this. Let's put the entire logic I just wrote inside of our typical method called main, and let me indent those three lines so that at least they're now combined into one main method. But instead of using input on line 2 an input on line 3, don't we go ahead and assume, for the moment, that we've got some function called get_name in the world, and let's go ahead and assume we've got another function like get_house in the world that don't take parameters. But their purpose in life is, by their name, going to be to get the user's name and to get their users house, respectively. And then I'm going to print out the exact same f string as before. I, of course, need to implement these functions now. So let me go lower in my file and define a function called get_name. Nothing in these parentheses because it's not going to take a parameter. And I'm going to go ahead and do something like name equals input ("Name"), just like before, and then I'm going to go ahead and return name. So it's a super simple function, but it's an abstraction. I now have a function called get_name whose implementation details I don't have to care about anymore. I just know that the function exists. And I can tighten this up, in fact, . Don't really need a name variable on line 8 if I'm immediately going to return that same name variable on line 9. So let me just tighten this up a little bit even though it doesn't change the functionality and just immediately return the return value of the inputs function call here. Let's do something very similar now for get_house, which will similarly take no arguments. I'm going to go ahead and return the return value of input, this time prompting the user for their house. And I need one final detail. At the very bottom, let's continue our habit of doing if the name of this file equals, equals, quote, unquote, main, then let's go ahead and actually call main and recall that we have that in place so that, if this eventually becomes part of a module, a library of sorts, I don't accidentally call main blindly. I only do it if I mean to run main from the command line on this file. All right, so if I didn't make any mistakes here, let me go ahead and, in my terminal window, again, run Python of student.py, Enter. Let's type in Harry, Enter. Let's type in Gryffindor, Enter. And we're set. Harry from Gryffindor seems to still be working. So we haven't really solved the problem any more correctly, but I've laid the foundation to maybe now do some more interesting things because I've had these building blocks in place. But let me propose that we could be doing this a little bit differently. get_name, get_house is fine. But at the end of the day, I'm really trying to get a student from the user. I want their name and their house, not just one or the other. So maybe it would be a little cleaner still to define a function called get_student and let get_student do all of this work for us. Now, theoretically, get_student could call get_name and could call get_house. But because these functions are so short, I think I'm OK with just defining one function, called get_student, that similarly won't take any arguments. But it's going to do two things. It's going to get the students name, by prompting them with input as before. And it's going to get the student's house, by also prompting them as before. Now, hmm. I want to return the student, but I think I might have painted myself into a corner here because I now have two variables-- name and house. And yet, up until now, we've pretty much returned one or the other. We've returned one value. So any suggestions for how we can perhaps solve this problem that I just created for myself, whereby I want to return, really, a student, but I currently have a name variable and a house variable. I'd minimally like to return both of those. AUDIENCE: I believe that, we can return a dictionary, includes the name and the house. DAVID J. MALAN: Yeah, so we absolutely could return a dictionary-- a dict object in Python, whereby maybe one key is name; one key is house; and the values thereof are exactly the values of these variables. So we could totally do that. I worry that that might be getting a little complicated. I wonder if there's a simpler way instead. Any other instincts-- even if you're not sure it would work? AUDIENCE: Return both name and house? DAVID J. MALAN: Return both name and house. I like the sound of that. It sounds simple. I don't have to figure out what a dictionary is going to look like. And in fact, this, too, would be a valid approach, even if you've not seen this before. It turns out, in Python, that you can return multiple values, but that's a bit of a white lie. Or we could take Muhammad's approach of actually returning a dictionary and putting multiple keys therein. So here, again, we have yet another example of how you can solve the same problem in at least two ways, and I daresay we're about to see even more. So one way you could solve this problem, whereby you want to return multiple values, would be to do something like this. I could go ahead and literally return not just name, but I could put a comma and also return house. This is not necessarily something you can do in other languages if you have programmed in other languages before. It depends on the language. But it looks like, thanks to this comma, maybe I can, in fact, return two values as [INAUDIBLE] proposed. Well, if I'm returning to values in this way on line 10, how do I get both values at the same time? Well, there's a couple of ways. Let me go up to my main function. I know, minimally, I'm going to have to change the get_name and get_house to get_student. But what am I going to store the return value in? I think I could actually do this. And we have seen this technique before, where you can unpack, so to speak, sequences of values that are coming back. And indeed, consider this to be exactly that. name, house is some kind of sequence that I'm returning of values-- name, house. So if I want to unpack those and store the return values in two separate variables, I can, in fact, use the commas on the left-hand side of my assignment operator, the equal sign, to do just that. Now, to be clear, I don't need to call these variables name and house here. I could simplify this and use just n here and h here, and then I could return just n and h. But I would argue that's not very clear to the reader as to what's going on. So I think, in this case, even though it's a coincidence that I've used the same variable names in get_student and get_name, and in main, it's a little more readable to someone like me. So I'm going to leave it as is. Well, let's go ahead and see, now, if this works. Let me clear my screen down here and run Python of student.py, Enter. Let's again type in Harry. Let's again type in Gryffindor, Enter. And voila, we still see that Harry is from Gryffindor. But what are we actually doing here? What are we actually doing by returning this value? Well, it turns out that what we've just done is use a tuple. A tuple is another type of data in Python that's a collection of values-- x, y or x, y, z. It's similar in spirit to a list, in that sense, but it's immutable. It's not mutable. Now, what does that mean? A list, as we've seen it before, is a data structure in Python that you can change the values of. You can go into bracket 0 for the first location and change the value there. You can go to bracket 1, bracket 2, bracket 3 and actually change the values in lists. But if you have no intention of changing the values of variables and you want to return, effectively, multiple values, you don't have to even return it as a list. You can return it as a tuple instead, just by using a comma. And it turns out we can make explicit that-- here's the white lie. I'm not actually returning to values per se. Whenever you use a comma in this way on line 9, you're actually returning one value, which is a tuple. Inside of that tuple now are two values. So it's similar in spirit to returning one list with two thing Here I'm returning one tuple with two things. And the mere fact that I've used a comma and nothing else tells Python that I indeed want to return a tuple. But there's more explicit syntax that we can use instead. I can actually-- more verbosely-- put explicit parentheses around the values of this tuple just to make more clear to me, to the reader that this isn't two values per se. This is one value with two things inside of it. And what I can actually do then, too, is-- I don't have to unpack this up here, so to speak. I can actually go up here and maybe give a more apt name, like student, and I can name the value, or rather name the variable in which I'm storing the return value of get_student as, quote, unquote, "student." So maybe this is a little better design now because I'm sort of abstracting away what a student is. It's implemented at the moment as a tuple with two values. But at least, now I have a variable called what I mean, a student. But there's going to be a catch. On line 3, I still want to print out that student's name and their house. But I don't have a name variable anymore, and I don't have a house. And I also don't have a dictionary, as was proposed earlier, so I can't even go at those keys by name. But what a tuple is-- it's very similar in spirit to a list, but it is indeed just immutable. And what I mean by that is I can still index into it numerically by saying student [0] for the item in the first location in that tuple. And then over here, instead of house, I can say student [1]. student [1] is going to give me the second location in that tuple. Let me go ahead and clear my terminal window. Again, run Python of student.py. Let's type in Harry. Let's type in Gryffindor, Enter, and we still have some working code. Let me pause here now and see if there are any questions on this technique of returning a tuple and indexing into it in this way. AUDIENCE: I guess, what's an actual use case where you would use a tuple versus a list or something else that's similar? DAVID J. MALAN: It's a really good question. When would you use a tuple versus a list? When you want to program defensively, or, in general, when you know that the values in this variable shouldn't change, so why would you use a data type that allows them to be changed? It just invites mistakes, bugs down the line, either by you or colleagues who are interacting with your code. So tuple is just another way where you can increase the probability of correctness by just not letting anyone, yourself included, change the contents therein. So it's just another tool in your toolkit. But let's make clear, then, what I mean by "immutable." Again, I claim that "immutable" means that you cannot change the value. Well, let's go ahead and try to do this. Let me go ahead and run this program once more as is-- Python of student.py. Let me go ahead and type in, for instance-- how about Padma's name? And I'm going to go ahead and say that Padma is in Gryffindor as in the movies. And we see-- Padma from Gryffindor. But technically, I went down this rabbit hole in looking at Harry Potter more closely. Technically, in the books, Padma, I believe, was from Ravenclaw. So this is actually a mistake or an inconsistency between the movies and the books. Let's see if we can't fix this inconsistency in our code. So how about we do this? If the student's name that's inputted equals Padma, why don't we override whatever the house is and change it to be properly Gryffindor. Let me go ahead and do if student-- now, if I want to get at Padma's name, I'm going to have to do student [0]. I have to know what location the name is in in this tuple. But if that value equals equals Padma, let's go ahead with this if statement and make a change. Let's change the student's [1] value. So the second value, if we're zero indexing-- let's change it to be another house in the world of Harry Potter called Ravenclaw. So I'm just fixing maybe the user's input. They watched the movie so they type in Padma Gryffindor, but, mm-mm, in the books, it was Padma from Ravenclaw. All right, let me go ahead and go down to my terminal window, clear my terminal, and do Python of student.py, Enter. I'm going to do Harry as well as Gryffindor, just to demonstrate that that is still working as intended. Let me clear my screen again, though, and run Python of student.py on Padma, and I'll put her, too, in Gryffindor, as in the movies, and hit Enter. And now I just see a big mess of errors on the screen. Some kind of exception has been thrown. And indeed, a type error has happened. I'm using a data type wherein there's an error, and what is that error? Well, 'tuple' object does not support item assignment. It's a little arcanely expressed-- that is, it's not really very user friendly. But if you think about what those words mean, 'tuple" object does not support item assignment. So assignment is copying from right to left. So somehow, that's invalid. And here is a manifestation of the immutability of tuples. You cannot change location 0 or 1 or anything inside. That is a feature. That is the design of a tuple. So if I want to override that, I think I'm going to have to use a different type of data that we've used before-- namely a list, and that's fine. If you want to enable yourself and colleagues using your code to change the contents of that container, well, we can go ahead and return not a tuple using explicit parentheses or no parentheses, just the comma, but I can use square brackets. And if I'm using square brackets on the left and the right, this is indeed explicitly a list. Same idea, but it's mutable. That is to say you can change the contents of a list. So making no other changes, just returning a list with square brackets instead of a tuple with parentheses or just the comma. Let me go ahead now and run Python of student.py, Enter. Let me type in Harry and Gryffindor again. That's still working. Good to see. Let me run this once more and type in Padma and Gryffindor, as in the movies, but no, now we've corrected it to be Padma from Ravenclaw, as in the books instead. Any questions now on tuples versus lists or this idea of immutability versus mutability. AUDIENCE: Can we use a nested tuple in Python, like a nest list? DAVID J. MALAN: Absolutely. You can have not only nested lists in Python, where one of the elements in a list could be another list-- so you have some square brackets out here; you might have some other square brackets inside. You can absolutely do the same with a tuple as well. There is no constraint on the types of values you can put in there. We've not had occasion to do that in this case. I'm just returning a simple tuple with two elements. But yes, you could absolutely do that, too. Other questions on tuples versus lists? AUDIENCE: OK, for example, when I see the square brackets, is it mainly used for the list? DAVID J. MALAN: Oh, a really good question. Sort of. So when you create a value like a list, you use square brackets, and that would indeed be a visual indicator that this is definitely a list. If you instead see parentheses, that's a visual indicator, when creating a value, that it's definitely a tuple. However, somewhat confusingly, both lists and tuples use square brackets when you access the contents of them. When you index into them at location 0 or location 1 you always use square brackets. So that's the distinction there. Good question. Allow me to propose now, if I may, that we solve this problem yet another way, and let's see if we're either making things better or for worse than us. Recall that dictionaries, or dict objects, also exist in Python. And a dictionary is this collection of keys and values. And the upside, in particular, of a dictionary is that they have better semantics. You don't just have to assume that a name is always going to be at location 0; house is always going to be at location 1. That's the kind of thing, especially if you had three, four, or more values-- eventually you or someone is going to get confused and forget what the order is and you're going to write buggy code. So a dictionary is a little more powerful in that you can semantically associate keys, little descriptions, with the values-- those keys and those values, respectively. So let me go ahead and do this, and we can do this in a few different ways. But let me propose that we focus on get_student here . And let's go ahead and do this. Let me go ahead and delete the implementation of get_student as is. Let me create a student variable and initialize it to an empty dictionary. And I can do that with just two curly braces here. And then let me go ahead and set two keys inside of that dictionary. Inside of the student, there will be, quote unquote, a "name" key, and the value of that is going to be whatever the return value of input is when I prompt the user for their name. And then the "house" key, inside of that same student dictionary, is going to be the return value of whatever the user types in for their house. , And lastly I'm going to go ahead and return student. So now I am literally returning one thing, still, but this time, it's a dict rather than a tuple, rather than a list. But there are still two things in it, technically four things if you count the keys and the values. But there's two key value pairs. Now, my code up here is going to have to change a little bit. And let's simplify this and remove, for instance, now the Padma if statement just to focus on what's changing at hand. And let me go ahead now and leave line 2 alone. I'm still going to have a student variable that gets assigned the return value of get_student. But what I want to do here now is actually access the keys inside of that dictionary-- not by numeric index, which was for tuples and lists-- 0 and 1, but by way of the keys. Now, normally, I might be in the habit, as I personally am, of using double quotes-- quote, unquote, "name" inside of there and quote, unquote, "house" inside of there. But before I even run this code and show you a mistake-- see an error on the screen, does anyone want to call out what I have done wrong here? This is just an f string. I just want to print out the value of the name key, the value of the house key in this dictionary. AUDIENCE: [INAUDIBLE] DAVID J. MALAN: Your audio was a little garbled for us. But I think I heard double quotes and single quotes. So I'm going to assume that, indeed, you've identified precisely the issue. I'm just going to confuse Python right now. Even though this is an f string inside of double quotes, prefixed with an f, I can't actually use my double quotes inside my double quotes because that's going to potentially confuse Python. If I run this program now, Python of student.py and hit Enter, I get a syntax error. So the program didn't even run fully. It just couldn't be understood because it got confused by those double quotes. So the simplest fix here would indeed just be to use not double quotes but single quotes around the keys, or conversely, flip the double quotes on the outside to single quotes, then use double quotes in the inside. You just want to be consistent. So a subtle detail, but again, this is now specific to dictionary syntax. This isn't fundamental to how we're solving this current problem at hand. Well, let's go ahead and try this. Let me go ahead now and run Python of student.py. Let's go ahead and type in Harry. Let's type in Gryffindor. And hopefully, Harry is back from Gryffindor. No syntax errors. No other errors. I think I'm back in business here. And what I do like to be clear about using a dictionary is that it's allowing me just better semantics. And again, I don't have to remember, memorize, document that 0 is name; 1 is house. Instead, "name" is name and "house" is house. It's just a little clearer, a little more expressive. So that's generally a good thing, especially if we stored more data about students than just their name and their house-- if you had three fields, four, five, 10 different fields-- no one's going to want to remember or be able to remember forever which is zero which is 1, which is 2, and so forth. Better to introduce names, like "name" and "house" in this case. But let me tighten this up further. I'm typically in the habit of not introducing variables unnecessarily, unless they make the code more readable. And an alternative way to format the same code would be this. Strictly speaking, I don't need to create an empty dictionary, then add one key to it, then add a second key to it, and then return that dictionary. I can actually consolidate this all into one statement, if you will. Let me go ahead and do this. Let me go ahead and say name equals inputs return value, house equals inputs return value, and then, instead of returning any variable name student, which I'm going to propose doesn't need to exist anymore, let me just create and return the dictionary all at once. Let me do, quote, unquote, "name" in lowercase here, and then the variable. It's storing the user's name. Then, quote, unquote, "house," as my second key, the value of which is going to be house, the variable. Now, is this better? Maybe, maybe not. Maybe the first way was a little more readable, and that's totally fine to create variables if they improve the readability of your code. But just know that you can also create and return a dictionary on the fly like this, so to speak, all in one line, and I think it's arguably pretty reasonable in this case. Why? It's just pretty short. I probably wouldn't do this if it got longer and longer and longer. I might minimally then start moving my key value pairs to separate lines. But this would just be a slightly more compact way of doing this as well. But let me propose we do one more change. Let's go ahead and introduce that same special casing of Padma to fix her house from Gryffindor, for instance, to Ravenclaw. How do we do this with dictionaries? Well, dictionaries, like lists, are mutable. You can change what is in them, just like you can lists. How do you do that? It's just a little different syntactically. So let's go back into main and do this fix. If the student variable has a name key that equals equals Padma, then, indented, go ahead and change the value of the house key inside of that student dictionary to be, quote, unquote, "Ravenclaw" instead. So very similar in spirit to what we did with a list. But instead of using location 0 and 1, we're much more clearly, explicitly, semantically using, quote, unquote, "name" and, quote, unquote, "house," because you index into lists and tuples using numbers, but you index into dictionaries using strings, as I've done here. All right, let me go ahead and run Python of student.py. We'll again do Harry from Gryffindor. And I think all is well. Let me run it one more time this time with Padma, who, in the movies, is from Gryffindor, but should really be from Ravenclaw. Any questions then on this progression from tuples to lists to dictionaries? We haven't necessarily introduced anything new, other than those tuples, which have been available to us all this time. But the goal at the moment is just to demonstrate this distinction among these different data types and how they each work a little bit differently. AUDIENCE: What if a combination of lists is there in our tuple? We can change the list because are immutable but lists are mutable? DAVID J. MALAN: Correct. You can change the contents of lists, and you can put most anything you want in them-- other lists or strings, as I've done, integers, or anything else. Tuples you can do the exact same thing, but you cannot change them once you've created them. A dictionary is more like a list in that it is mutable. You can change it. But the way you index into a dictionary is by way of these keys, these strings, as we keep saying, rather than by numbers-- those numeric indices. All right, well, let me propose that there is yet another way of solving this problem. And I would argue that there's now an opportunity at hand. Even though this program isn't particularly complicated-- all I'm doing is collecting a name from the user and a house from the user-- you could imagine wanting, longer term, to collect even more information, like the student's patronus or magical spell or a whole bunch of other information that might belong in a student. And right now, we're just using these very general purpose data types in Python-- a tuple to combine some values together; a list to do the same, but let us change it later; a dictionary, which is more powerful because it's a little more structured. It does have keys, and it has values, not just values. But you know what? We wouldn't have to be having this conversation if the authors of Python had just given us a data type called student. Wouldn't it have been nice if there were just a type of variable I could create in my code called student? Then we wouldn't have to figure out, well, do we use a tuple or a list or a dictionary? But that's pretty reasonable. You can imagine just how slippery of a slope that is, so to speak, if the creators of a language had to anticipate all the possible types of data that programmers like you and me want to store in your programs. So they just gave us these general purpose tools. But they gave us another general purpose tool that's going to allow us to create our own data types as well and actually give them names, and that terminology is a class. A class is like a blueprint for pieces of data objects. A class is a mold that you can define and give a name. And when you use that mold or you use that blueprint, you get types of data that are designed exactly as you want. So in short, classes allow you to invent your own data types in Python and give them a name. And this is a primary feature of object oriented programming, to be able to create your own objects in this way and, in the case of Python in classes, even give them some custom names. So what does this mean in real terms? Well, let me go ahead and come back to VS Code here, and let me propose that we introduce a little bit of new syntax. I'm going to go ahead and clear my terminal window first. I'm going to go to the top of my file, and I'm just going to start a thought but not finish it yet. I'm going to use this new keyword for classes, called, literally, class, so the new keyword we're going to have here. And if I go back to our slides here, this would be the official URL where you can read up more on this particular feature of Python. In the official tutorial, class is a new keyword we can use. Now, this is coincidentally related to students because students take classes, but it has nothing to do with the fact that we're dealing with students. Class is a general purpose term in a lot of languages-- Python among them-- that allow you to define these custom containers with custom names for pieces of data. So let's go back to VS Code. Let's use this new keyword. And let me propose that we create a class called Student. And by convention, I'm going to use a capital S here, and I'm going to go ahead, and with a colon, get to, later, the implementation of this class. So I'm just going to use dot dot dot, which is a valid placeholder for now, that just indicates to me that I'm going to come back to implementing this later. But as of now, it does, in fact, exist. I now have a student class defined for me that I can now use in my code here. How am I going to use it? Well, first of all, let me go down to get_student, and let me change this code to no longer use a dictionary but to use this class. I'm going to do this. I'm going to give myself a variable called student, as I've done before, but I'm going to set it equal to capital Student (). So I'm going to do what appears to be calling a function and that function, Student with a capital S, notice, matches the name that I gave this class at the top of my file. All right, what do I next want to do? I'm going to go ahead and give this student a name. Now, if I were still using a dictionary, I would say student, quote, unquote, "name," using square brackets. But this is not a dictionary. It turns out classes have what, for now, we'll call attributes, properties of sorts that allow you to specify values inside of them. And the syntax for that happens to be a dot. We've seen dots before. We've used it in the context of modules and libraries, more generally. This is another similar in spirit use of a dot that allows you to get at something inside of something else. So student.name is going to be the syntax I use for giving this student a name. And that name is going to be whatever the return value of "Name" is. And then I'm going to go ahead and say student.house to give another attribute called "House" and give that the return value of input here, prompting the user for house. And then, as before, I'm just going to return student. But now what's really powerful about class, and object-oriented programming more generally, is that I've created this custom data type called, literally, Student, capital S. I've stored one such student in a variable like I can always do in a variable called student, lowercase s. But I could call it anything I want. It just makes sense to call it student as well, but lowercase for clarity. And then I'm returning that variable. And because of my syntax in lines 14 and 15, that has the result of putting inside of that class a name attribute and a house attribute. I just need to make one more change up here. I'm going to go ahead and remove our Padma code, just so we can focus only on what's new, rather than fixing her house. And I'm going to go in here and change the syntax that previously was for dictionaries. Again, dictionaries use square brackets and then strings in quotes-- either single quotes or double quotes, depending on the context. Here, though, I'm going to change this to be student.name, and over here, I'm going to change it to be student.house. And that's just going to be my new syntax for getting the contents of what appears to be a class called student. Let me go ahead and rerun Python of student.py, Enter. Let's type in Harry's name as before. Let's put him in Gryffindor, crossing our fingers as we often do, and Harry is indeed from Gryffindor. What, though, have I done? Let's introduce one other bit of terminology here it turns out that I can create a class, using that class keyword. But any time you use a class, you're creating what are called objects. And here is the word objects, as an object-oriented programming, or OOP. Let me go back to my code here. And even though I haven't really implemented much of it at all-- I literally just left it with a dot, dot, dot-- that's enough code, lines 1 and 2, to just invent a new data type called Student, capital S, that may or may not have some future functionality as well. That's enough to create a class. What, though, am I doing on line 11? On line 11, what I'm technically doing is creating an object of that class. So this, too, is another term of art. You create objects from classes. So if we go back to that metaphor, that a class is like a blueprint for a house or a class is like a mold, an object is when you use that blueprint to build a specific house or something that comes out of-- in plaster, the mold, when you actually use that mold to create such an object. So a class is, again, the definition of a new data type. The object is the incarnation of, or technically instantiation of. And another term for objects would actually be an instance. You have instances of classes as well. So that's a lot of vocabulary. But at the end of the day, it just boils down to this. You can define your own class, which is really your own data type. You can then store attributes inside of it, using this dot notation here. And then you can access those same attributes using code like this here. And now, I have a proper "student" data type, and I don't have to hack something together using a tuple or a list or even a dictionary. I now have a proper data type called "student" that the authors of Python didn't give me; I gave myself. Any questions now on classes, this new keyword, class, or this idea of these objects or instances thereof? AUDIENCE: Is the class object mutable or immutable? DAVID J. MALAN: A good question. And we've clearly laid the stage for having that conversation about every data type now. We will see that they are mutable, but you can make them immutable. So you can get the best of both worlds. Now, by writing some actual code-- and we'll write more code than the dot, dot, dot in just a bit, other questions on classes or these objects thereof? AUDIENCE: Then what would be the properties of those classes? DAVID J. MALAN: So at the moment, the properties of-- or the attributes of, as I've been calling them thus far-- would just be "Name" and "House." It turns out that there may very well be other attributes built into classes that we may see before long. But for now, the only two attributes that I care about are the ones that I myself created-- namely "Name" and "House" or, again, what I would call attributes. And in a little bit, we're going to start calling those same attributes, more technically, instance variables. "Name" and "House," as I presented them here in VS Code are really just variables called "name" and called "house" inside of an object whose type is student. All right, so what more can we do with these classes? Well, again, on line 11 is where we're instantiating an object of the student class and assigning it to a student variable. We're then adding attributes-- "Name" and "House," respectively-- on lines 12 and 13 currently. Both of those have values that are technically strings or strs, because that's what the return value of the input is. But those attributes values could actually be any data type. We're just keeping things simple and focusing on defining students in terms of two strings-- "Name" and "House." And then, on line 14, we're returning that variable. We're returning that object to main so that we can actually print out who is from what house. Well, let's go ahead and add a bit more functionality here because, right now, on lines 12 and 13, this is a little manual. And it's a little reckless of me to just be putting anything I want inside of this student object. It turns out with classes, unlike with dictionaries, we can actually standardize, all the more, what those attributes can be and what kinds of values you can set them to. So let me go ahead and do this. Let me propose that it would actually be really nice if, instead of doing this here, let me go ahead and simplify my code as follows. Let me go ahead and give myself a local variable called name and set it equal to the return value of input, like we've done many times now already. Let me give myself one other variable for now, called house, and set it equal to the return value of input as well, prompting the user for their house. And now, instead of creating a student object from my student class and then manually putting the name attribute inside of it and the house attribute inside of it, let me actually do something more powerful. Let me do this. Let me call that Student function, which is identical to the class name-- just by defining a class, you get a function whose name is identical to the class name, with the capital letter included. But instead of just doing open parenthesis, closed parenthesis, let me pass in the name that I want to fill this object with and the house that I want to put in that object as well. And now let me set the return value as before to be student equals like this. So what have I done that's different? Fundamentally, I'm still getting user input in the same way. I'm using input on line 11 and input on line 12. And I just so happen to be storing those return values in local variables. And now we're setting the stage for the more powerful features of classes and object-oriented programming more generally. Notice that I'm deliberately passing to this capital S Student function, name, house-- I'm passing in arguments to the function. Now, the student class is not going to know what to do with those yet, but now I'm standardizing how I'm passing data into this student class. And ultimately, it's going to give me an opportunity to error check those inputs, to make sure that the name is valid, that it has a value and it's not just the user hitting Enter. It's going to allow me to ensure that it's a valid house, that it's Gryffindor or Hufflepuff or Ravenclaw or Slytherin or not just hitting Enter or some random value that the user types in. Because I'm passing "Name" and "House" to the student class, this particular function, I'm going to have more control over the correctness of my data. So let's now go up to the student class, which, up until now, I left as just dot, dot, dot. It turns out that, in the context of classes, there are a number of not just attributes or instance variables that you can put inside, but also methods. Classes come with certain methods, or functions inside of them, that you can define, and they just behave in a special way, by nature of how Python works. These functions allow you to determine behavior in a standard way. They are special methods in that sense. Now, what do I mean by this. Well, let me go back to VS Code here. And let me propose that I start to define a standard function called underscore underscore, or Dunder, as it's abbreviated, init, underscore underscore, and then I'm going to go ahead and do open parentheses, and then I'm going to put in here, literally, the word self. More on that in just a moment. But now, inside of this function, I'm going to have an opportunity to customize this class's objects. That is to say this underscore, underscore init method, or Dunder init method is specifically known as an instance method, and it's called exactly this. This is designed by the authors of Python. And if you want to initialize the contents of an object from a class, you define this method, and we'll see what it's about to do here. Let me go back to VS Code, and let me do something like this. self.name = name, and self.house = house. But I don't want to just init this object very generically. I want this method, called init, to take in not just self but name, house as well. Now, what in the world is going on? Because there's a lot of weird syntax here. There's this Dunder init method-- double underscore, init, double underscore. There's, all of a sudden, this parameter called self. And then there's this new syntax-- self.name and self.house. Now you're seeing really a manifestation of object-oriented programming. It's not all that different fundamentally from what we've been doing for weeks with dictionaries, by adding keys to dictionaries. But in this case, we're adding variables to objects, a.k.a. instance variables to objects. Now, what's going on? Let's do this in reverse. Let's go back to the line of code we wrote earlier. On line 15, I am treating the name of this class-- Student with a capital S-- as a function. And I am passing in two values-- "Name" and "House." What I've highlighted here on the screen, on line 15, is generally known as a constructor call. This is a line of code that is going to construct a student object for me. Using synonyms, it is going to instantiate a student object for me. And again, how is it going to create that object? It's going to use the student class as a template, as a mold of sorts so that every student is structured the same. Every student is going to have a name. Every student's going to have a house. But because I can pass in arguments to this Student function, capital S, I'm going to be able to customize the contents of that object. So if you think about the real world-- if you've ever been on a street or a neighborhood where all of the houses look the same but they might be painted differently; they might be decorated a little bit differently on the outside, all of those houses might have been built using the exact same blueprint-- a mold, if you will. But then you can specialize exactly the finer points of those houses. By painting the outside a different color or planting different trees, you can style them differently. Similar in spirit here, we have a Student blueprint that's always going to have now a name and a house, but it's up to you and me to pass in any name and any house that we want. Now, where is this function? The fact that I'm calling Student, capital, S and then a parenthesis and a closed parenthesis with arguments inside suggest that there's a function somewhere in the world that has been defined, with def, that's going to be called. Well, as you might have guessed by now, the function that will always be called, by definition of how Python classes work, is a function called double underscore, init, double underscore. Why? It's a crazy name, but it's what the authors of Python chose to just implement the initialization of an object in Python. Now, the only weird thing-- especially weird thing, I will admit, is this. It would be way clearer, to me, too, if the only two parameters for init we're just name, house. That's how we've defined every function thus far in the class. You just specify the parameters that you want the function to accept. And that lines up with what I'm doing on line 15. I am only passing in two things to the student function. But it turns out that the authors of Python need to give us a little bit of help here because suppose that you pass in "Name" and "House" to this init method. And a method is just a function inside of a class. What are you going to do with the name and the house? Literally, where are you going to put them? If you want to remember the name and the house for this student, you've got to be able to store those values somewhere. And how do you store them in the current object that has just been "instantiated?" Well, the authors of Python decided that the convention is going to be that this init method also, semi secretly, takes a third argument, that has to come first. By convention, it's called self, but you could call it technically anything you want. But the convention is to always call it self. And self, as its name implies, gives you access to the current object that was just created. What does that mean? Again, now, on line 14, now that it's moved down a little bit, this line here is a constructor. It constructs a student object. But there's nothing in that object initially. There's no name; there's no house. But the object exists in the computer's memory. It's up to, now, you to store the name and the house inside of that object. How do you do that? Well, Python will just automatically call this init method for you, and it's going to automatically pass in a reference to an argument that represents the current object that it just constructed in memory for you, and it's up to you to populate it with values. And what this means is that, inside of your init method, you can literally do self.name to create a new attribute, a.k.a. an instance variable, inside of that otherwise empty object and put this name inside of it. It allows you to do self.house and store that value of house. Now, you could call these things anything you want. They could be n. They could be h, as before. But that's really not very self-explanatory. Much better to do this kind of convention. self.name equals name. self.house equals house. And this is like installing into the otherwise empty object the value name and house and storing them in, really, identically named instance variables in the object. And again, an object is just an instance of a class. Now, I know that was a lot of vocabulary. That's a lot of weird syntax. So any questions on this init method, whose purpose in life, again, is to initialize an otherwise empty object when you first create it? AUDIENCE: So what is the difference between the init method and default constructor? DAVID J. MALAN: A good question. So in other languages-- if you programmed before. For instance, Java-- there are functions that are explicitly called constructors that construct an object. They initialize it with values. Python technically calls this init method the initialization method. It initializes the value. It's on line 15 now of my code, if I scroll back down, that I'm technically constructing the object. It turns out there's another special method in Python, that we won't talk about in detail today, called underscore underscore, new, underscore underscore that actually handles the process of creating an empty object in memory for us. But, generally speaking, you, the programmer, don't need to manipulate the new function. It just works for you. Instead, you define your own init method here and init function inside of your class, and that method initializes the contents of the object. So there's technically a distinction between constructing the object with new and initializing it with init. But in the world of Python, you pretty much only worry about the init method. Python generally does the other part for you. A good question. Others? AUDIENCE: What about if you want to store more than one name or more than one house? DAVID J. MALAN: A good question. If you want to store more than one name or more than one house, you can do this in different ways. You could create other attributes-- technically called instance variables-- like self.name1, self.name2. But we've seen, in the past, that that is not a very good design, just to have multiple variables to store multiple things. Maybe, instead, you have an instance variable called self.names, plural, and you set it equal to a list of names or a list of houses. Now, in this case, I don't think that really solves a problem because I'm trying to implement a student, singular, so it doesn't really make sense to have multiple first names. Maybe a nickname, maybe a last name, so we could add those, too. But I don't think we need multiple names per se and, in this case, multiple houses. But absolutely, you could do that using some of our familiar building blocks like lists. Other questions? AUDIENCE: How are classes or objects represented in memory? DAVID J. MALAN: How are classes and objects represented in memory? So the class is technically just code. It is the code on the top of my file-- lines 1 through fou4-- that defines that blueprint, that template, if you will. Objects are stored in the computer's memory by taking up some number of bytes. So you're probably familiar with bytes or kilobytes or megabytes. There's some chunk of bytes, probably all in the same location in the computer's memory or RAM, where those objects are stored. But that's what Python, the program, handles for you. Python the interpreter figures out where in the computer's memory to put it. You and I, the programmers, get to think and solve problems at this level. Python, the interpreter, handles those lower level details for you. How about one final question on classes and objects? AUDIENCE: So my question is if we can the same do the same thing with the dictionaries, so why to use classes? DAVID J. MALAN: Good question. If you can do the same things as you can with dictionaries, why should you use classes? Because we are just scratching the surface now of what you can do with classes. Allow me to go back, now, to my keyboard and show you more of what you can do with classes. But in short, you can do much more with classes. You can ensure the correctness of your data much more with classes. You can error-check things. And generally, you can design more complicated software more effectively. And we'll continue to see, today, features of Python and object-oriented programming more generally that allows us to do just that. So let me propose, in fact, that first, let's just tighten up this current implementation, which again has us with an init method that just declares two instance variables-- self.name and self.house, house, which, again, just creates those variables inside of the otherwise empty object and assigns them values-- name and house, respectively. Let me go ahead and just do one little thing here. I don't really need this student variable. Let me just tighten this up so that each time we improve or change the code, we're focusing, really, on just the minimal changes alone. So I've not fundamentally done anything different. I just got rid of the variable name, and I'm just returning the return value of this student function that's constructing my new object for me. So I'm just tightening things up as we've done many times in the past. Well, what if something goes wrong in creating this student? For instance, what if the user does not give us a name, and they just hit Enter when prompted for name. I don't want to put in my computer's memory a bogus student object that has no name. I'd ideally like to check for errors before I even create it so I don't create a nameless student. It would just be weird and probably a bug to have an object that has no name. Similarly, I don't want the user to be able to type in something random as their house. At least in the world of Harry Potter, there's really only four houses, at Hogwarts at least. There's, again, Gryffindor and Hufflepuff and Ravenclaw and Slytherin-- a list of four valid houses. It would be nice if I somehow validated that the user's input is indeed in that list. Now, I could do all of that validation in my get_student function. I could check, is the name empty? If so, don't create the student object. Is the house one of those four houses? If not, don't create the student object. But that would be rather decoupled from the student itself. get_student currently exists as just my own function in my student.py file. But classes-- and really, object-oriented programming-- more generally encourages you to encapsulate, inside of a class, all functionality related to that class. So if you want to validate that a name exists-- if you want to validate that a house is correct, that belongs just fundamentally in the class called student itself, not in some random function that you wrote elsewhere. Again, this is just methodology because, again, if we think about writing code that gets longer and longer, more and more complicated, it should make just intuitive sense that, if you keep all the house-- all of the name and all of the house-related code in the student, it's just better organization. Keep all of the related code together, and that's probably going to set you up for more success. And indeed, that's part of this methodology of object-oriented programming. Let me go ahead now and change my students classes init method to do this. If the name is blank-- so if not name-- and we've seen this kind of syntax before. If you say in Python, Pythonically, if not name, that's doing something like this. If name equals, equals, quote, unquote-- but I can do this a little more elegantly. Just say, if not name, would be the more Pythonic. Well, I want to return an error. I might want to do something like this. Print missing name. But this is not good enough. It does not suffice to just print out missing name and then let the rest of the code go through. All right, well, what could I do instead? In the past, we've seen another technique I could do sys.exit, and I could say something like missing name, and I could go up here and I could import sys. But this is a really obnoxious solution to the problem. Just because you or maybe a colleague messed up and called a function with an invalid name, you're going to quit my whole program? That's really, really extreme of a response, and you probably don't want to do that if your program is in the middle of running. You might want to clean some stuff up. You might want to save files you don't want to just exit a program sometimes in some arbitrary line, just because input was invalid. So I don't think we want to do that either. But we do, now, have a mechanism for signaling errors. Unfortunately, I can't do something like this. I could try returning none and say, uh-uh, this student does not exist. I'm going to hand you back none instead. But it's too late. If we scroll back down to where I'm creating the student, it's on line 17 now where I've highlighted this code. The student has already been created. There is an object somewhere in the computer's memory that's structured as a student. It just doesn't have any values inside of it. But it's too late, therefore, to return none. That ship has sailed. The object exists. You can't just suddenly say, nope, nope, there is no object. There is an object. It's up to you to signal an error. And how do you signal an error? Well, we've actually seen this before, but we haven't had occasion to create our own errors. It turns out, in Python, there's another keyword related to exceptions that Python itself uses to raise all of those exceptions we've talked about in the past. When you've caught things like value errors or other such exceptions that come with Python, well, it turns out you, the programmer can raise-- that is create your own exceptions when something just really goes wrong-- not wrong enough that you want to quit and exit the whole program, but enough that you need to somehow alert the programmer that there has been an error. Something exceptional, in a very bad way-- something exceptional has happened, and let them try to catch that exception as needed. So let me go back to VS Code here and propose that, if the user passes in an invalid name-- it's just empty, so there's not a name. Well, what I really want to do is this. I want to raise a value error. And we've seen the value errors before. We've created value errors accidentally before. And generally, you and I have tried to catch them if they happen. Well, the flip side of this feature of exceptions in a language like Python is that you, the programmer, can also raise exceptions when something exceptional happens. And you can even be more precise. You don't have to raise a generic value error and let the programmer figure out what went wrong. You can treat value error and all exceptions in Python like functions and actually pass to them an explanatory message like, quote, unquote, "Missing name," so that at least the programmer, when they encounter this error, knows, oh, I messed up. I didn't make sure that the user has a name. And now, what do you want to do instead? Well, now, if you're the programmer, you could do something like this. You could try to create a student except if there's a value error. Then you could handle it in some way. And I'm going to wave my hand with a dot, dot, dot, at how you would handle it. But you would handle it using try and accept, just like we have in the past, and that would allow you, the programmer, to try to create the student. But if something goes wrong, OK, I'll handle it nonetheless. So what's new here, again, is this raise keyword, that just lets you and I actually raise our own exceptions to signal these errors. Well, let me go back to my code here, and I'm just going to go ahead and not bother trying or catching this error. For now, we'll just focus on raising it and assume that, from our recon exceptions, you could add try and accept as needed in places. Let me go back to the code here and propose that something else could go wrong with house. If there is a name, we're good. But if we're given a house but it's invalid, we should probably raise an exception for that, too. So what if we do this? If house is not in the list containing "Gryffindor," quote, unquote, "Hufflepuff," quote, unquote-- let's see, "Ravenclaw," quote, unquote, or "Slytherin," quote, unquote, then, with my colon, let's raise another type of value error. But rather than raise a generic value error, let's pass in an argument, quote, unquote, "Invalid house." And so here we now see a capability that we can do with classes that we can't with dictionaries. If you add an attribute to a dictionary, a key to a dictionary, it's going in no matter what. Even if the name is empty, even if the house is a completely random string of text that's not one of these four houses, it's going into that dictionary. But with a class, and by way of this init method, you and I can now control exactly what's going to be installed, if you will, inside of this object. You have a little more control now over correctness. And so now let me go ahead and scroll back down to my terminal window and clear it. Let me run Python of student.py. Let me type in something like Harry. Let me type in Gryffindor, Enter, and we see that, indeed, Harry is from Gryffindor. What if I made a mistake, though? What if I ran Python of student.py and typed Harry as the name, but this time typed in Number Four, Privet Drive, which is where he grew up, instead of his proper Hogwarts house. Let me hit Enter now, and now you see a value error. But this isn't one that Python generated for us, per se. I raised this error. And therefore, if I went in and wrote more code in my get_student function, I could also catch this error with our usual try except syntax. So all we have now is not just classes in our toolkit, but even more powers when it comes to exceptions, and not just catching them ourselves but raising them ourselves, too. Any questions now on this use of classes and init and now this ability to raise exceptions when something goes wrong inside of the initialization? AUDIENCE: So what if the user has a middle name-- name, middle name, and last name? How would you fix that? DAVID J. MALAN: Good question. If you wanted the student to have a first name, middle name, and last name, we could do this in a bunch of different ways. The simplest, though, if-- let me clear my screen here, and let me just temporarily do this. Let me propose that the init method take in a first argument, a middle argument, and a last argument. And then what I think I would do down here is ultimately have first = first, and then I would do the same thing for middle and last. So middle and middle, and then last and last. And then what I would have to do here is, when I actually ask the user for their name, I might need to really go all out. I might need to ask them first for their first name and store that in a variable called first, and therefore pass in first. I might similarly need to ask them for their middle name and store that in a variable and then pass in a second argument, middle. And then lastly, if you will, let me go ahead and create a third variable called last, get the input for their last name, and pass that in as well. I could instead just use one input and just ask them for their whole name. So type in David Malan, Enter, or David J. Malan-- all three of them, and maybe I could use Python's split function, maybe a regular expression to tease it apart. That's probably going to be messy because there's going to be people who don't have just two or three names. They might have four or five. So maybe sometimes it's better to have multiple prompts. But that's not a problem because, with a class, we have the expressiveness to take in more arguments if we want. We could even take a list if we wanted. But I think we'd probably want to have even more error checking then, not just for name but for first, and then maybe for middle, and then maybe for last. So it just is more and more code, though there would be ways to perhaps consolidate that, too. Let me undo all of that and see if there are other questions now on classes. AUDIENCE: I assume classes are something I might do at the beginning of a project. Can I just put them in a different file and import them into my project, or my main code as needed? DAVID J. MALAN: Absolutely. A really good question. You could imagine wanting to use this student class, not just in student.py but in other files or other projects of yours. And absolutely, you can create your own library of classes by putting the student class in your own module or package, per our discussion in the past about libraries more generally. And absolutely, you can do that. And later today, what we see is we've actually been using classes-- you and I-- before, in third party libraries. So you, too, can absolutely do the same. How about one more question on classes? AUDIENCE: Can you have optional variables in classes? And two, can you have your own error names, like-- let's be egotistical and say I want to raise Eric error? DAVID J. MALAN: Short answer, yes. These init functions are just like Python functions more generally, even though they're special in that they're going to get called automatically by Python for you. But if you wanted to make house optional, you could do something like this. You could give it a default value in the init function's signature so to speak-- in that first line of code on line two. And that would allow me to not have to pass in house. In this case, I'm going to continue to always pass in name and house, but you could make things optional. And yes, to your second question, if you wanted to have your own error message, like an Eric error, you could actually create your own Eric error exception. And we'll see, in a little bit, that there's actually a whole suite of exceptions that exist, and you, too, can invent those as well. Let me propose, though, that we now introduce one other aspect of this whereby we try printing out what a student looks like. At the moment, if I scroll back down to my main function, I'm still printing the student's name and house very manually. I'm going inside of the object, doing student.name, and I'm going inside of the object again and getting student.house, just to see where the student is from. But wouldn't it be nice if I could just print the student, like I've been printing for weeks-- any, int, or float, or str, or any other data type? Well, let's see what happens if I just try printing the student, instead of manually going inside and trying to create that sentence myself. Well, in my terminal window-- let me go ahead and run Python of student.py again. Let me type in Harry. Let me type in Gryffindor. And voila, Harry-- whoa, OK, main student object at 0x102733e80. Well, what is going on? Well, if you were to run the same code, you might actually see something different on your computer in terms of that number. But what you're really seeing is the underlying representation, as a string, of this specific object. In particular, you're seeing where in the computer's memory it is. This number, 30x102733e80, refers to, essentially, a specific location in the computer's memory or RAM. That's not really that interesting for me or you or, generally speaking, programmers, but it's just the default way of describing, via print, what this thing is. But I can override this as well. It turns out that there are other special methods in Python when it comes to classes-- not just underscore underscore, init, underscore underscore, but, continuing in that same pattern, underscore underscore, str, underscore underscore. So this, too, is a special method that, if you define it inside of your class, Python will just automatically call this function for you any time some other function wants to see your object as a string. Print wants to see your object as a string. But by default, if you don't have this method defined in your class, it's going to print out that very ugly esoteric incarnation thereof, where it says main__.Student object at 0x, dot, dot, dot. Well, how can I then define my own str function? Well, here, back in VS Code, let me propose that I go in and define not just __ init, but let me define a second function in this class here, as follows-- def __ str __. There are two. Even though the font in VS Code is putting the two underscore so close, it just looks like a longer underscore. There are indeed two there, on the left and the right just like for init. This one only takes one argument that, by convention, is always called self so that you have access to it. And then, indented below that after a colon, I'm going to go ahead and create a format string and return it. So let me go ahead and return-- how about something generic first like "a student." So I'm not going to bother even trying to figure out what this student's name or house is. I'm just going to always return "a student." Let me go back now to my earlier code, which has print (student) on line 16. Let me clear my terminal window and rerun Python of student.py, Enter. Type in Harry, type in Gryffindor. Last time, I saw that very cryptic output. This time, I see, more generically, "a student." More readable but not very enlightening. Which student is this? Well, notice that the double underscore str method takes in this self argument by default. It's just the way the Python authors designed this method. It will always be passed a reference to the current student object. What do I mean by that? When this line of code on line 6 is called, print, because it's hoping it's going to get a string, is going to trigger the underscore underscore, str, underscore underscore method to be called. And Python, for you, automatically is going to pass into that method a reference to the object that's trying to be printed so that you, the programmer, can do something like this. Here's an f string with double quotes as usual. I'm going to use some curly braces and say print out self.name from self.house. So there's nothing new in what I've just done. It's just an f string-- an f on the beginning, two double quotes, a couple of pairs of curly braces. But because, automatically, this str method gets passed self, so to speak, a reference to the current object, I can go inside of that object and grab the name. I can go inside that object again and grab the house. So now, when I go back to my terminal window-- previously it just printed out a student. But now, if I run Python of student.py, Enter-- type in Harry, type in Gryffindor, and one more time hit Enter, Harry is again from Gryffindor. But if I run this yet again-- let's, for instance, do Draco is from Slytherin, Enter. Draco's from Slytherin. Now it's customized to the specific object that we're trying to print. Questions on this function here-- this Dunder str method. AUDIENCE: Is there anything else that the underscore underscore, str method can do? The other question is, what's the difference between str and repr. DAVID J. MALAN: A good question. So there are many other methods that come with Python classes that start with underscore underscore. We're just scratching the surface, and we'll pretty much focus primarily on these. But yes, there are many others, and we'll see at least one other in just a little bit. Among the others is one called repr, which is a representation of the Python object. Generally speaking, the underscore underscore, repr, underscore underscore method is meant for developers' eyes. It typically has more information than "Harry from Gryffindor." It would also say what type of object it is, like a student, capital S, whereas underscore underscore, str, underscore underscore is generally meant for users-- the users of the program, and it's meant to be even more user-friendly. But both of those can be overridden as you see fit. Well, let me propose now that we pick up where we've left off on student and just add even more functionality, but not just these special methods like double underscore init and double underscore str. Let's create our own methods because therein lies the real power and flexibility of classes if you and I as the programmers can invent new functionality that's specific to students. For instance, students at Hogwarts, over the time in school, learn how to cast a certain type of spell. So when they say, Expecto Patronum, something comes out of their wand that typically resembles an animal or something like that. It's a special spell that they have to practice and practice. So let's see if we can't store, not just the student's name and their house, but also their "patronus," what, actually, they conjure when using this spell. Well, let me go ahead and clear my terminal window. And in the top of my code here, in the init method of Student, let me go ahead and start expecting a third argument, in addition to self, which automatically gets passed in, called patronus. And I'm not going to worry, for now, on validating the patronus from an official list of valid patronuses, or patroni. I'm instead going to go ahead and just blindly assign it to self.patronus = patronus, and we're going to let the user type whatever they want for now. But I could certainly add more error checking if I wanted to limit the patronus to a specific list of them here. Let me go ahead now and prompt the user for this patronus, as by-- in my get_student function-- defining a variable called patronus or anything else, prompting the user for input for their patronus. And now I'm going to go ahead and pass in that third variable here. So again, similar in spirit to just adding more and more attributes to the class, I'm going to pass in all three of these values instead of just two. I'm not going to do anything interesting with that value yet. But just to make sure I haven't made things worse by breaking my code, let me run Python of student.py. I'll type in Harry. I'll type in Gryffindor. And it turns out his patronus was a stag, . And hit Enter I haven't seen what his patronus is in my output because I didn't change my str method yet. But at least I don't have any syntax errors. So at least I've not made anything worse. But suppose, now, I want to have functionality, not just for initializing a student and printing out a student. If my class is really meant to be a student, what I can do is not just remember information about data about students. What's powerful about classes, unlike dictionaries alone, is that classes can have, not just variables or instance variables-- those attributes we keep creating. They can also have functions built in, a.k.a. methods. When a function is inside of a class, it's called a "method," but it's still just a function. At this point, we've seen two functions already-- two methods-- called a double underscore init and double underscore str, but those are special methods in that they just work. If you define them, Python calls them automatically for you. But what if you wanted to create more functionality for a student so that your class really represents this real world, or maybe "fantasy" world notion of a student, where students not only have names and houses and patronuses; they also have functionality. They have actions they can perform like casting a charm, a spell, magically. Could we implement, therefore, a function called charm, that actually uses their magical knowledge? Well, let's go ahead and define our very own function as follows. Let me clear my terminal window, scroll back up to my student class. And instead of creating yet another function that's special, with double underscores, I'm going to invent my own function, or method, inside of this class. I want to give Harry and Hermione and all of the other students the ability to cast charms, so I'm going to define a function that I can completely, on my own, call charm. I could call this function anything I want. But because it's a method inside of a class, the convention is that it's always going to take at least one argument, called self, by convention so that you have access to the current object, even if you don't plan to use it, per se. All right, let me go ahead and propose that we implement charm in such a way that the method returns an emoji that's appropriate for each student's patronus. How to implement this-- well, inside of the charm method, let's go ahead and match on self.patronus, which is the instance variable containing a string that represents each student's patronus. And in the case that it matches a stag, for instance, for Harry, let's go ahead and return maybe the closest emoji-- this horse here. How about in the case of an otter? Well, in that case, let's go ahead and return, oh, maybe the closest match to the otter, which might be this emoji here. And let's see, in the case of a-- for Ron, rather than Hermione-- a Jack Russell terrier, let's go ahead and return-- don't have as many options here. Why don't we go ahead and return the cutest available dog in that case. And in the case of no patronus recognized, as might cover someone like Draco, let's go ahead and use a default case using the underscore as in the past, and let's go ahead and return for this-- oh, what should happen if someone doesn't have a patronus? Why don't we just see a magical wand that seems to fizzle out, as in this case? All right, well, now, rather than just print the student, let's go about printing their actual patronus. So I'm going to go down to my main function here. I'm going to still get a student, using the get_student function. But rather than print student, let's go ahead and declare "Expecto Patronum!" printing out just that as pure text. And now let's go ahead and print out, not the student but, rather, the return value of their own charm method. So let me go back down to my terminal window and run Python of student.py and Enter. Name-- let's start with Harry. He lives in Gryffindor. Patronus is a stag. And let's see-- Expecto Patronum! And of course, we'd see the stag emoji. What about someone like Draco, who, at least in the books, doesn't have a known patronus? Well, let's go ahead and clear my terminal window, rerun Python of student.py, and this time, let's type in Draco for name, Slytherin for house, and Patronus is unknown. So I'm just going to go ahead and hit Enter. And now, Expecto Patronum! And it just of sizzles instead. Well, let me propose, now, that we remove this patronus code just to simplify our world and focus on some of the other core capabilities of classes. So at the risk of disappointing, I'm going to get rid of all of these beautiful emoji and charms, and I'm going to go ahead and stop asking the user now for their patronus. And I'm going to stop passing it into init here. And I'm going to stop doing this here. And I'm going to instead just go ahead and restore our use of print student here, and I'm going to go ahead and get rid of patronus down here. So just essentially undo all of the fun charms we just created. So we're now back at the point in the story where we have a student class, with only two methods-- init and str. The first of those takes, of course, self as the first argument as it always will, plus two more now-- name and house, no more patronus. We're validating name up here. We're validating house down here. And then we're assigning name and house, respectively, to two instance variables called name and house also. But we use self to get access to the current object, to store those values therein. We then still have our str method here, which takes one argument-- by default, self, and that's it. And that function is going to be called automatically any time you want to convert a student object to a string, just like print might want to do here. So let me go ahead and just make sure I haven't broken anything. Let me run Python of student.py. I'll type in Harry. I'll type in Gryffindor, Enter. OK, we're back in business. Gone are the charms and patronus, but at least I'm back to a situation where I have names and houses. But it turns out, at the moment, our use of classes is not very robust, even though we have this mechanism, very cleverly, if I may, in our init method of making sure that we're validating name and house, making sure that name is not blank, and making sure that house is a valid house among those four Hogwarts houses. It turns out that classes will still let me get at those attributes, those so-called instance variables, using dot notation anyway. Let me scroll down then and try to do this a little adversarially. Suppose that online 16 I go ahead and call get_student, which exists as before, and then I store the return value in a student variable-- again, on line 16. That will ensure that get_student gets called, which calls input and input. And then it calls the student constructor, which invokes, automatically, this init method. So by way of how we've laid out my code, we're going to ensure that name is not blank and house is definitely one of those four values. My error correction-- or error checking is in place. But if I'm a little adversarial, I can still circumvent it. Suppose that-- fine, going to require me to type in Harry and Gryffindor? I'm going to go ahead and type in student.house equals, quote, unquote, "Number Four, Privet Drive," and you're not going to be able to stop me. Why? Well, it turns out, with classes and objects thereof, you and I can still access those instance variables using this familiar dot notation. That's how we began the story of classes-- just setting these attributes ourselves. But you can also read these attributes themselves and change them later if you want. And this will effectively circumvent the if condition and the other if condition in our init method because that is only called when you first create the student object. There's nothing stopping me, at the moment, from just changing the house or the name after. So if I now clear my terminal window and run Python of student.py, I'll still type in Harry and Gryffindor to meet my requirements that the house be one of those four. But when it's printed, notice, I've still overridden it. So it seems that, while classes do allow us a little more control over the data we're storing, it doesn't necessarily prevent the user-- or rather the programmer-- be it myself or maybe a colleague, from still messing things up. So here, too, in the spirit of programming a little more defensively, allow me to introduce another feature of Python as well-- namely properties. So a property is really just an attribute that has even more defense mechanisms put into place, a little more functionality implemented by you to prevent programmers, like me and you, from messing things up like these attributes. So again, a property is going to be an attribute that you and I just have more control over. How? We just write a little more code, using some Python conventions. And how we're going to do that is going to use, in just a moment, a feature-- a keyword known as @property, which is technically a function. Property is a function in Python. But we're about to see some new @ syntax that allows you to decorate functions. And this, too, is a term of art. In the world of Python, you can have decorators, which are functions that modify the behavior of other functions, if you will, and we'll leave it at that without going too much into the weeds. And we'll see, by, example how you can use these decorators, specifically to define properties. So let me go back to VS Code here. And let me propose that I do this. I'm going to go ahead and create-- how about a property called house as follows. Inside of my student class, I'm going to go ahead-- and below my init method and below my str method, I'm going to go ahead and define a function called house that takes, as it always must, one argument at least, called self. And what I'm going to do now is return self.house. So I'm just going to define a method called house, whose sole purpose in life is to return the value of house. But I'm going to define one other method, curiously also called house, but that's going to take into, as arguments, two values-- self as always and also a value called house. And I'm going to now do this. I'm going to do self.house = house. Now, what have I done? Well, let me just temporarily add some comments here. In a moment, we're going to start referring to this generally as a getter. And down here, I'm going to refer to this as a setter. And this is terminology frequently see in the world of Java. Some of you have programmed in Java before. But as the names imply, a getter is a function for a class that gets some attributes. A setter is a function in some class that sets some value. And now, even though we're not done, and there's a bit of a mistake in the code I've already written, intuitively, what we're going to do is this. We're trying to prevent programmers, myself included, from circumventing my error checking that I put into place for name and house. How can I do that? Well, we don't have that many building blocks in programming. We have things like variables for data, and we have functions for actions. Well, why don't we do this? Why don't we somehow require that, in order to access an attribute, you go through some function. And let's require that, in order to set some attribute, you go through some function. And conventionally, those functions are called a getter function and a setter function. And why are we using functions or, in this case, methods inside of a class? Well, once you have functions, those are just actions or verbs that you and I can create ourselves. We can put any error correction I want in these functions because it's code that's going to get executed top to bottom. So how can I now prevent the user from setting the house to an invalid value? Let me borrow some logic from before rather than blindly do this-- just set self.house equal to the house value that's passed in-- let's add our error checking there. So if house is not in the following list of Gryffindor or Hufflepuff or Ravenclaw or Slytherin, just as before, let's go ahead and raise a value error, just to signify that, uh-uh, something has gone wrong. I'll be more explicit. I'll include a message like, "invalid house," quote, unquote. Otherwise, I'm going to proceed on, now, line 21 to set self.house to house. So I've just copied, if you will, or retyped my error checking inside of this so-called setter function. Now, why have I done that? Well, to be clear, whenever the user or the programmer writes code like this, student.house equals, what's about to happen magically is Python will not just let the programmer access student house directly-- that attribute, that instance variable, a.k.a. self house. It's instead going to magically automatically call this setter function for me. How does Python know to do that? Well, if it's see that, on the left-hand side, there is self.house, where house is the name of the getter or setter, and then it sees an equal sign, indicating assignment, that's just enough of a visual clue to say, wait a minute. I'm not going to let you access that attribute directly. I'm going to use the setter instead. Why? Because the equal sign means I'm trying to set. I'm trying to assign a value from right to left into that attribute. So what Python's is going to do automatically is call this function for me. And that's amazing because now I can execute code-- an algorithm to check, do I want to let the user-- the programmer set that attribute to that value? If not, I'm going to raise a value error, and you're just not going to be able to do it. If so fine. I'll go ahead and set it for you. But in order to do this, we need a little more syntax. And I'm going to get rid of my comment, and I'm going to use that decorator. I need to tell Python to treat this method as a getter. And then the syntax for the setter is a little different. You now say house.setter. I wish one was getter and the other was setter. That's not the way they designed it. When you want to define a getter, you just say @property above the function. And you name the function exactly like you would like the property to be called-- quote, unquote, "house." Once you do that, you can now use a new decorator that's automatically created for you called @house, because I called it house. And then you literally say, @house.setter. And this whole line, on line 17, is a clue to Python that here comes a function, whose name is identical-- but notice that it takes two arguments-- both self, so you have access to the contents of the object, and house, which is just going to be a str that comes from the programmer from the human input return value so that you can set that value as well. But there's one fix I need to make now, here. Everything else, I think, is still good. However watch this. I no longer need this error check here. Why? Because, if I scroll back down to my code here, I claimed a moment ago that code like this, with student.house equals, is going to automatically get Python to call my setter for me. Guess what? Even up here, in my init method, calling self.house equals is also going to call my setter method, which is amazing because now I can keep all of my error checking in one place in the setter, and it will now get called either when I create the object for the first time, because of init, or even if the programmer tries to circumvent that init method and change the value of this attribute, my setter will also get called. My setter will get called any time I access .house. But there's one fix I need to make. Unfortunately, I have collided names. Right now, if we go up here, on line 5, this is an instance variable. It's a string inside of my self, inside of the current student object, called name. And this is another instance variable called house. Unfortunately, if I have an instance variable called name and house, I cannot also have functions called house. They're going to collide. You've got to decide. Do you want the variable to be called house? Or do you want the function to be called house? Unfortunately, you can't have both because now Python is going to confuse one for the other. So the conventional fix for this is to do this-- to have the setter not store the value that's passed in self.house, but to use an almost identical name, but to use a little indicator that means you know doing this correctly. You typically, by convention, put an underscore in front of the instance variable's name. And when you return it up here, you similarly put an underscore. So now, technically, my instance variable is called _house, but my property, which is a fancier attribute, if you will, is called house alone. Huge amount of syntax, I know, but it's a very powerful feature. And again, this is why you can graduate from dictionaries alone and have so much more functionality at your disposal. Let me go ahead and clear my terminal window and run Python of student.py, Enter, name. All right, let's go ahead and type in Harry. Let's go ahead and type in Gryffindor. Crossing my fingers as always. And now, look, "Invalid house." This is a good thing. Why? Because, notice, in my main function, I'm still trying, maliciously, if you will, to change Harry's house to not be one of the four valid ones. I'm trying to change it to his childhood home of Number Four, Privet Drive. But because Python knows that, wait a minute, you're trying to assign-- that is, set a value-- and that value, a.k.a. house, is now defined as a property you're going to have to go through the setter function instead to even let you change that value. And because I have this raise ValueError. If the house is not as intended, you're not going to be allowed to change it to an invalid value. So I'm protecting the data on the way in, through the init method, and I'm even defending the data if you try to override it there. So I think the only solution for me, the programmer, is, don't try to break my own code. Let me remove that line because it's just not going to work. Let me run Python of student.py and, again, type in Harry; type in Gryffindor, Enter, and Harry's indeed from Gryffindor. If I did something incorrect, like Harry from Number Four, Privet Drive, Enter, we're again going to see the value error because my code just doesn't let that value in via manual input now or via that adversarial change. All right, that was a lot. But any question on properties? AUDIENCE: Why we are using getter then setter? It's just for the purpose so that we can find that method, that function in our code. DAVID J. MALAN: The reason that I'm going through the trouble of defining this getter or setter is because I want to make sure that programmers cannot do things like this. If I'm going through the trouble of validating the attributes for these student objects, I don't want you to be able to go in there and just change them at will. I want to have some control over that object so that you can just trust that it's going to be correct as designed. So using a getter and setter really just enables Python to automatically detect when you're trying to manually set a value. The equal sign and the dot, as I've highlighted here, is enough of a clue to Python to realize, wait a minute, you're trying to set a value. Let me see if this class has a setter defined. And if so, I'm going to call that, and I'm not just going to blindly assign the value from right to left. So it's just giving me more control. Other questions on properties. AUDIENCE: When we use getters, we just have just one argument. And if we use setters, it's always going to be two arguments? Is that normal? DAVID J. MALAN: Correct. It's always going to be one argument-- self for the getter, two arguments for the setter-- self and something else. And the intuition for that is, if you're getting a value, you don't need to pass anything else in because you already know the object. It's called student in this case. So you're just going to get the value of that property. But if you want to set the property to something else, you've got to pass in that argument. You've got to pass in the value to which you want to set it. So it's always 0 or 1. However, you see it as 1 or 2 because, again, any function inside of a class, a.k.a. a method, is going to be automatically passed self so that you have access to that current object in memory. How about one other question on properties? AUDIENCE: Why didn't we use the same underscore house init method? DAVID J. MALAN: A good question. So even though I'm using the underscore house here, in my setter, and the underscore house here, in my getter, I deliberately did not use it up here. The reason for that is that, by using self.house and this equal sign, that's the same pattern that I want Python to recognize. I want Python to automatically call the setter, even when I'm passing in the house via the init method. If I were to change this to do this, that would circumvent the setter, and now there's no error checking in init whatsoever. So it's such a fine line. The only thing standing between us and error checking or no error checking is the presence or absence of this underscore. But that's typically the convention. By not using the underscore there, make sure that even that assignment goes through the setter so that, honestly, I, don't have to copy paste the same error checking in two places. I can put it just in the setter. So it's a better design, and that's why I manually retyped it at first, but then I deleted it from init. Well, allow me to propose that we make one other change to this file. Might as well go ahead and define a property for name as well. And let me go ahead and do this-- maybe above the house property just to keep things in the same order as I defined them earlier. Let me give myself another property. This one is going to be called name. It's going to take one argument called self, as always. And this one, very similarly, is just going to return self._name. So I'm going to anticipate that I'm going to have to rename name also so that I don't have that same collision as before. But now let me go ahead and define another setter-- this one for name. So the convention is @name.setter. Why name? Because the property I just created is called name. So the getter and setter work in conjunction in this way, if you will. Let me go down under that name setter and define another function, also called name. But the key thing here is that it's not identical. It's not the exact same function name and the exact same number of arguments. The setter, again, takes a second argument. And I can call it anything I want, but I'm going to call it name because that's what's being passed in. And I'm going to put my error checking here. If not name, just like we used to do, let's go ahead and raise a value error, and let's put an explanatory message like "Missing name," quote, unquote. Otherwise, let's go ahead and update self._name to equal name. And I don't have to change init except to get rid of this duplicate error checking now because, again, if I use self.name equals here and self.house equals here with no underscore, both of those assignments are going to go through my two setter functions now. Before we run this, let me go ahead and remove this adversarial code, which we know won't work because we're catching it. Let me go back down to my terminal window and run Python of student.py, Enter. Let's type in Harry. Let's type in Gryffindor. And that seems to work. Let's try though, again, to run Python of student.py with Harry from Number Four, Privet Drive. This will not work. A value error with invalid house, because that's not one of the four Hogwarts houses. And now, for good measure, let's run it one more time. And let's not even give it a name. Let's just hit Enter when prompted. I can type anything for the house. I'll go ahead and still give it Gryffindor, Enter. And now we get another value error, but this one is for missing name. So we seem, now, to have all the more of a defense mechanism in place to ensure that name is as we expect. It's got to have some value that's not blank. And house is as we expect. It's got to have one of those four values. But at the risk of bursting everyone's bubble and making you wonder, why did we just go through all of that, unfortunately Python really focuses on conventions, not hard constraints. And by that, I mean this. If I go back into my main function after I've gotten a student on line 30 and I try to adversarially do something like this-- student.house equals "Number Four, Privet Drive," we know this won't work because my setter for house is going to catch this. Watch again. Python of student.py. Let's type in Harry. Let's type in Gryffindor, which will at least pass our check that's induced by init. But line 31 is going to trigger the same setter to be called, and we're going to raise a value error saying "Invalid house." Unfortunately, and if some of you are already thinking a little adversarially, tragically, look what you can do. You can change .house to be ._house. Why? Well, the instance variable is now called _house. The property is called house, no underscore. But the underlying attribute implemented as an instance variable is still called _house. And tragically, Python of student.py. Let's type in Harry. Let's type in Gryffindor, which is correct. But watch what happens now. Oh, my God. We slip through. So what was the point of all of this emphasis from me on doing things the "right way," the Python quick way by having this getter and setter? Well, unlike languages like Java, that just prevent you from doing things like this, Python itself allows you to specify that certain instance variables can be public and accessible to anyone's code, or protected, or private, which means that no one else should be able to change these values. In the world of Python, it's just the honor system. It's not baked into the language itself that there's a notion of visibility, public or private or even somewhere in between protected. Instead, you're on the honor system. And the convention generally is, if an instance variable starts with an underscore, please don't touch it. Just don't. That's on you if you touch that variable and break things. The underscore is meant to signify a convention that this is meant to be "private," but it really just means, please don't touch this. Sometimes, if there's two underscores, which you can use, too, that's an even greater effort by programmers to say, really don't touch this. But technically speaking, there's nothing stopping you or me from circumventing all of these mechanisms, these properties, these getters and setters. We're ultimately just on the honor system not to do so when we see instance variables prefixed with one, or perhaps even two underscores. All right, so this is a lot all at once-- this Introduction to object-oriented programming. But it might come as quite a surprise that, even though we might have identified OOP by name in weeks past, we've all been using classes and objects for weeks now in this class. In fact, if you think back on one of the very first things we did in this class, we used integers and just got integers from the user. But if you haven't already-- if you go and dig into the documentation for integers, which, again, lives at this URL here, you would actually find that int itself is and has been for weeks a class. And in fact, this is the signature of the constructor call for an int, whereby you pass in x, like a number, quote, unquote, "50" or, quote, unquote, something else-- you pass in optionally the base-- 10 for decimal, 2 for binary or anything else. And that int function will actually return to you, all this time, an object of type int. That is to say int is a class. It is a template, a blueprint for creating integers in memory. And any time you and I have converted a string, for, instance to an int, you and I have been creating an object of type int that was calling, apparently, the underscore underscore, init, underscore underscore method, that someone else-- the authors of Python-- wrote to give us back that proper integer. Besides that, if you can believe it, strs, strings in Python have been classes since the first week of this class as well. If you look up the documentation for a str, which lives at a similar URL there, you will find that, when you instantiate-- that is, create a str-- it takes, optionally, a parameter called object here, the default value of which is just, quote, unquote, which allows you to create, in effect, an empty string, a blank string, if you will. But any time you and I have created strs or even used explicitly the str function, you are getting back an object of type str. Any time you and I have forced a string to lowercase per the documentation, using syntax like this, you and I have been taking an object of type str and forcing it all to lowercase by calling a method called lower, a method that the authors of Python built into the str class, but it's been there from the get-go, so this notion of methods is not even new today. You wouldn't have been doing it for this long. If you've ever called strip to remove the leading and the trailing whitespace from a string in Python, you are calling another method that came with Python-- written by the authors of Python. And even though we didn't call it a class at the time, a str, all this time, has been a class. And instances of strings are, themselves, objects. And those objects come therefore with these functions built in-- a.k.a. methods that allow us to do things like force to lowercase and strip whitespace from the beginning and end. Let's do another. list-- any time you've created a list, either syntactically with square brackets or literally with L-I-S-T, open parentheses, closed parentheses, which is also possible, you have been using a class. If you go to the documentation for list, at this similar URL here, or more specifically, the tutorial on lists here in Python, you will see that a list is and has been, since the early weeks of this class, a class itself. And that list class takes, as part of its initialization, an optional iterable, something that can be iterated over-- like 1, 2, 3, or some list of values, and you can then get back a list containing those same iterable values. If you've ever appended something to a list in this class, as I have myself in the past, you've been using a method called append that comes with the list class that, per the x here, takes an argument that allows you to append something to the current list, a.k.a. Self in the context of that method. We can do this all day long. If you've used a dictionary or a dict in Python-- I've actually, all this time, been calling them dict objects, and that's for a reason. dict itself is a class in Python, if you pull up its official documentation here. And you'll see that it is defined, indeed, as itself a class. And that class comes with methods as well. And so any time we've manipulated dictionaries, we've been underneath the hood, using all of those same methods. And in fact, we can see this if we're really curious. Let me go back over here to VS Code. And let me go ahead and create a new file that, very simply, does something play around with data types. And let me go ahead and create a new file, for instance, called, say, type.py, just so that I can poke around inside of some values. And in type.py, I'm just going to go ahead and do this. I'm going to print out whatever the type is of, say, the number 50. And This is a function you've not necessarily seen me use already, and it's not one you would frequently use in your own code. There are other ways to detect, if you need to, what the type is of a variable. But in this case, type of 50 is just going to tell me and then print out what the data type is of that value. Now, hopefully, all of us could guess that 50 is indeed going to be an integer-- that is, an int, but we can see it in this way. And this, too, is what's powerful about knowing a bit of programming. If you want to know the answer to a question, just try it out, like I am here. So let me go ahead and run Python of type.py, Enter. And there it is. When you print out the type of the number 50, you'll see on the screen, in this cryptic syntax, class 'int.' This is not something that you probably want to show to the user. But if you yourself just want to poke around and see what's going on or maybe use that information somehow, it's certainly at your disposal to use this type function for that. Let's change it around a little bit. Instead of passing as the argument to type 50, as an int, let's type something also familiar, like "hello, world," in double or single quotes. Let me go back to my terminal window, clear the screen, and run Python of type.py again. And now, voila, there it is. All this time, a str is also a class. We can do this a few more times, For. Instance. Let's go ahead and change "hello, world" to just an empty list-- open square bracket, closed square bracket. And this is starting to look a little cryptic, but, again, notice what I'm doing. In square brackets is an empty list. We've done that before. That is the sole argument to this new type function. And that's just being passed to the print function so that the return value of type is the argument to print. So if I now run this code, Python of type.py, there it is. A list is a class, too. You might recall that I said that you can also create an empty list by literally doing list (). This is a bit of an inconsistency, as we can now identify that int and str and now list-- they're technically all lowercase. And I went to great lengths of creating my student class to have that capital S. That's a convention. Because int and stir and list and others come with Python, they decided to make their built-in data types-- even though they're classes-- all lowercase. But the convention, the recommendation in the Python community when creating your classes is to capitalize the first letter, as I did, in something like Student, capital S. But list () is identical to really just two empty square brackets. If I clear my screen and run type.py again, you see the exact same thing. The class is called list. Let's do one more. Let me change the list to be not square brackets but curly braces. We've done this before. Any time I've done two curly braces with nothing in between, this, of course, is an empty dictionary, or a dict object in Python. Well, we can see that now. Let me clear my screen, run Python of type.py, Enter, and there it is-- class 'dict." It's been there this whole time. We just didn't call it a class until today. I can similarly do this one explicitly. Instead of two curly braces, let's write out dict with two parentheses. Now we have a lot of parentheses again, like with list. But this is just making even more clear that the type of a dict object is indeed the class, dict, itself. So this is to say that, as new as a lot of today's idea and syntax, might be you've actually been using it, perhaps unbeknownst to you, for weeks now. We now just have terminology to describe what it is we've been doing all this time. And you now have the expressiveness, with some practice, to create your own classes, inside of which are your own instance variables, perhaps wrapped with those properties and your own instance methods. But it turns out there's other types of methods in the world. Thus far, I've been deliberate in calling all of our variables instance variables and all of our methods instance methods. It turns out there's other types of variables and methods out there, and one of those is called class methods. It turns out that sometimes it's not really necessary or sensible to associate a function with objects of a class, but rather with the class itself. An instance, or an object of a class, is a very specific incarnation thereof. Again, on that neighborhood that has a lot of identical looking buildings, but they're all a little bit different because of different paint and such, sometimes you might have functionality related to each of those houses that isn't distinct or unique for any of the houses. It's functionality that's going to be exactly the same no matter the house in question. Same in the world of object-oriented programming. Sometimes you want some functionality, some action to be associated with the class itself, no matter what the specific object's own values or instance variables are. And for that, we have a keyword called @classmethod. This is another decorator-- really, another function-- that you can use to specify that this method is not, by default, implicitly an instance method that has access to self, the object itself. This is a class method that's not going to have access to self, but it does know what class it's inside. So what do I mean by this? Well, let me go back to VS Code here. And let me propose that we create a new file this time implementing the notion of a-- the sorting hat, from the world of Harry Potter as well, to stay on theme. I'm going to go ahead and run code of hat.py. And in hat.py, let's implement the notion of the sorting hat. If unfamiliar in the books and in the films there is literally a pointy hat that, when a student put it's on their head, that sorting hat, so to speak, decides what house the student is in-- whether it's Gryffindor or something else. So let's implement, in code, this notion of a sorting hat such that, when we pass to the sorting hat the name of a student, like, quote, unquote, "Harry" this sorting hat, implemented in code, will tell us what house that student should be in. Well, let's go ahead and do this. In hat.py, first, let's go ahead and define a class called hat, and then let's get back to implementing it itself. And I find this to be a helpful technique, not just with teaching but when writing code. I know I want a hat class. I don't necessarily know what I want it to do yet, so I'm going to create this placeholder, dot, dot, dot, so I'll come back to that. Let's now try to use this class as though it existed. And from there, I perhaps can realize exactly what functionality that class needs to have to support my use case. Let me go ahead and create a variable called hat in all lowercase and instantiate a hat object. So no matter what the hat class ends up looking like, this is the common syntax for instantiating an object of a certain class. In the past, we saw student, all lowercase, equals capital Student, open parenthesis, close parentheses, and then eventually, we added in things like name and house. For now, let's assume that the hat is much simpler than a student, and it only has sorting capabilities. So I'm not going to even pass any arguments there, too. Let me assume that the sorting hat has one function-- one method inside of it called, sort. And so if I do hat.sort ("Harry"), let's propose that that prints out what house that student should be in. So that's it. I'm going to encapsulate-- that is tuck away inside of a hat class-- all of this requisite functionality, and I'm going to print out onto the screen what hat-- what house Harry belongs in. Now I think I need to get into the weeds of actually initializing this class. Well, let me go ahead and do this. If I don't care to parameterize hat-- I just want to, for instance sort values, let's go ahead and define this function, sort, first. So let's define sort, as taking a first argument, self, which is always going to be the case when defining an instance method as before. But the sort method clearly takes one argument from the programmer, me-- namely the student's name. And again, we've seen this dichotomy before. Even though I'm trying to pass in one argument, when I define the method, it's got to take that many arguments, plus one more-- self which is always going to be automatically passed in by Python first. What do I want to do? Well, let's go ahead and do something like this. Print, this name-- how about "is in," "some house." I'm going to, again, use some placeholder code for myself because I'm not quite sure how to finish implementing this sorting hat. But I think that's enough to just test where my code is at now. Let me go ahead and run Python of hat.py and hit Enter. And it looks like, indeed, Harry is in some house. We're not done yet because it's clearly not doing anything interesting, but it at least is running correctly with no errors. Well, let's go ahead now and decide where-- what house Harry should actually be in by introducing a bit of randomness and choosing a house randomly. While I can do this in a few ways, Let. Me go ahead and do this. I need to have a list of houses somewhere. So where can I put that? I could solve this problem in different ways. Let me propose that I do this. Let me define a method called init, as I've done before, that takes in self, but no other arguments. And whenever the sorting hat is instantiated, let's do this. Let's create a houses instance variable, plural, that equals this list-- Gryffindor, Hufflepuff, Ravenclaw, Slytherin, so the exact same list that we've used before, and I'm storing it in an instance variable inside of this class. I'm not taking any arguments beyond self to init, but I just need this list of values somewhere, for instance. So what can I do here? Well, let me go ahead and replace some house with the actual house. Well, what could I do here? Well, I want to put a house there. Well, let's go ahead and create a variable called house. And if you think back to our discussion of libraries, in the random module, there is a function called choice that, if you pass in a list of choices, like self.houses, that will pick a random house out of those four. And then on line 7, I can pass it in. If I want to tighten this up, let me just go ahead and highlight that code, get rid of the variable. It's technically unnecessary. And because the line of code is still pretty short, I'm OK with just putting it all in one line. But I could certainly use the variable like I did a moment ago. So what have I done? In my init function, I have defined a initialization of the object that stores in self.houses the list of four houses. And then, in sort, I'm accessing that same list, but I'm randomly choosing the set of houses there. Now, why have I done it in this way? This, too, is general convention. Any time you have a list of things that-- who knows? Maybe will change over time. Places like Harvard have constructed new houses over the years, so you might have to change the list of available houses. It didn't happen in seven books or eight films of Harry Potter. But you could imagine maybe Hogwarts eventually has a fifth house, so there's generally some value in putting list of constants toward the top of your file, toward the top of the class so it's just obvious what the list of values is. You don't want to necessarily tuck it away in some function, like sort, especially if you might want to use that function-- sorry, especially if you want to use that list in multiple functions, not just sort. But if I kept adding to this class, you might want to use that same list of houses in multiple functions. So let's keep it in the object itself by storing it in self.houses. All right, well, we're about to change the course of history here perhaps. Let me do Python of hat.py, and I think we're about to assign Harry to one of those four houses randomly. Huh, NameError. Name 'random' is not defined. Well, wait a minute, where did I go wrong here? Thinking back to our class on libraries, why did my code break and not tell me where Harry is to be? AUDIENCE: You did not import the random library. DAVID J. MALAN: Exactly. If the random library or module is something I want to use, I need to tell Python that at the top of my file. So let me go up here and do import random. And then, below that, let me go ahead and clear my terminal window and try again. Python of hat.py, crossing my fingers, seeing where Harry is going to end up. And, OK, Harry as of now is officially in Hufflepuff, despite everything you've read or seen. Well, let's run this again. Let me clear my window and run Python of hat.py, and now he's in Ravenclaw. That's consistent with using random. Let's clear that and run it again. He's still in Ravenclaw, but that could happen, even though there's four choices. Let's do it again. Hufflepuff-- back in Hufflepuff. We can't seem to get the right answer. Now he's in Gryffindor, albeit randomly. So we seem to have a program that, based on these limited tests, seems to be assigning Harry to a house randomly. Now I'm somewhat lazily just letting sort print out this value. I could do something else, like return a string, and then let me, on line 13, do the printing for me. But for now, I think we have an example of a class called hat that, nonetheless, applies some of our lessons learned thus far today, where I've created a class-- because a sorting hat is, frankly-- well, I was about to say real world entity, but really a fantasy world entity. And indeed, that's a, perhaps, common heuristic or mental model to have. When should you use a class to represent something in your code? Very often, when you're trying to represent some real world entity or fantasy world entity, like a student, which is something in the real world, like a sorting hat, which, OK, doesn't exist, but hat's certainly do, so quite reasonable to have a class for hat. And that's not always the case that classes represent real world entities. But we've seen thus far that int and stir and list and dict-- these are all structures that you might have in the real world. We have integers and strings of text and other things. So it rather makes sense to represent even those things, more technically, using a class as well. You could use just a dictionary to represent a student or a hat. But again, with classes come all this and even more functionality. But I honestly am not using classes in, really, the "right way" here. Why? Well, in the world of Harry Potter there really is only, to my knowledge, one sorting hat. And yet, here I have gone and implemented a class called hat. And again, a class is like a blueprint, a template, a mold that allows you to create one or more objects thereof. Now, most of my programs Thus far have been pretty simple, and I've just created one student. But certainly, if I spent more time and wrote more code, you could imagine writing one program that has a list of students-- many more students than just the one we keep demonstrating. Yet it would be a little weird-- it's a little inconsistent with the real or the fantasy world of Harry Potter to instantiate one, two, three or more sorting hats. There really is just one. Really one singleton, if you will, which is a term of art in a lot of contexts of programming. So let me propose that we actually improve the design of the sorting hat so that we don't have to instantiate a sorting hat because right now this is kind of allowing me to do something like hat 1 = hat, hat 2 = hat, hat 3 =, and so forth. I don't really need that capability. I really just need to represent the sorting hat with a class, but I don't really need to instantiate it. Why? Because it already exists. I need just one. So it turns out, in Python, that, up until now, we've been using, as I keep calling them, instance methods-- writing functions inside of classes that are automatically passed a reference to self, the current object. But sometimes you just don't need that. Sometimes it suffices to just know what the class is and assume that there might not even be any objects of that class. So in this sense, you can use a class really as a container for data and/or functionality that is just somehow conceptually related-- things related to a sorting hat. And there's this other decorator or function called @classmethod that allows us to do just this. So let me go back to my code here. And let me propose that, if I'm not going to instantiate multiple houses, I don't really need this init method because that's really meant to initialize specific objects from that blueprint, that template, that mold. So let me get rid of this. But if I get rid of this, I no longer have access to self. But that's OK because it turns out, in addition to their existing class methods, there are also what we might call class variables. And class variables exist within the class itself. And there's just one copy of that variable for all of the objects thereof. They all share, if you will, the same variable-- be it an int or str or, in this case, a list. So what I've done here is define, inside of my hat class, in a class variable called houses-- I don't say self because self is no longer relevant. Self refers to specific objects. I want a variable inside of this class, a.k.a. A class variable that equals that list. Because it's inside of this hat, now, class, I can use that list in any of my functions. I've only got one now, called sort. But if I had more, it would be accessible to all of those methods as well. And with sort, it also doesn't really make sense to sort within a specific sorting hat because, again, I only want there to be one. So I can actually specify that this is class method by saying @classmethod. And I don't pass in self anymore. I actually, by convention, pass in a reference to the class itself. It's typically written as cls. Why? Well, if you wrote C-L-A-S-S, that would actually conflict with the keyword "class" that we keep using up here. So the world realized that, oops, we can't reuse that same phrase here. So let's just call this class. This is useful in some contexts including this one. Why? Well, notice what I can now do. I can now change self to be just class. Why? Because houses now-- not an instance variable, accessible via self.houses. It is now a class variable, accessible via class.houses, or technically cls.houses in this case. But now the final flourish is this. Now, I don't have to instantiate any hat objects as I used to on here, line 13. I can just use functionality that comes with this class. So I'm going to delete that line altogether. I'm going to capitalize the hat on this new line 13 and just say hat.sort, ("Harry"). So what have I done? I've not bothered instantiating an object of type, hat. I am just accessing a class method inside of the hat class that-- you know what? Is just going to work. This is how class methods work. You use the name of the class, capital letter and all, dot method name, passing in any arguments you want. Python is going to automatically pass in some variable via which you can refer to that class in that function that you've implemented inside of that class so that I can do something like this. It's not that I want a variable called houses locally in this function, I want the variable called houses that's associated with this current class so I can still access this same list that I defined on line 6. And now, if I go back down here to my terminal and run Python of hat.py, Enter, Harry is still in Hufflepuff once more. Harry is still in Hufflepuff once more. Harry is back in Gryffindor, at least randomly. Questions, now, on these class variables or these class methods, which are in contrast with instance variables and instance methods. And the one thing, at least, that's a little strange here is that, even though there's a decorator called @classmethod, there is not one called @instancemethod. A method is just automatically a so-called "instant method" when you define it without any decorator. AUDIENCE: Can you have a class inside another class? DAVID J. MALAN: You can. You can define one class inside of another. Generally speaking, this isn't done, but there are cases where it can be helpful, especially for larger, more sophisticated programs. So yes, it is possible. Other questions. AUDIENCE: The question was about the self.houses. When we remove it and we pass data, variable is created itself, s why we remove the self? DAVID J. MALAN: So in the previous examples-- both of the hat demonstration and also all of the student demonstrations-- we were creating a student object by calling Student, capital S, open parenthesis, close parenthesis, with, eventually, name and a house passed in. And then we were using the double underscore init method to initialize the self.name and the self.house instance variables therein to those respective values. In this latest version of the sorting hat, I haven't bothered with self anywhere, only because, conceptually, I don't need or want there to be multiple hats in the world. I'm just using the class as a container to bundle up this list of houses, this sorting functionality. Maybe eventually all add more functionality to it. But that's it. And so sometimes you can use object-oriented programming in this somewhat different way when you want there to be functionality but it's not specific to any one specific hat. It's specific to the sorting hat itself. How about one other question now, on these class variables or methods-- just another way of using object-oriented programming but to solve a somewhat different problem? AUDIENCE: Well, what's the difference between the class hat and a function of hat? DAVID J. MALAN: A good question. So why are we using a class at all and not just having a file called hat.py with a variable called houses and a function called sort? Why are we adding this complexity? In this particular case, we don't necessarily need to. I could absolutely go in here. I could get rid of the class. I could undo this indentation. I could get rid of this decorator. And I could get rid of hat dot. And I could just do this and additionally let's say, let's get rid of class here. Let's get rid of class here. And now run Python of hat.py, Enter, and it still works. Put Harry in the wrong house, but that's what we have-- what happens randomly. That's fine, too. What we're introducing today, by way of object-oriented programming, is just a different way of modeling the world. It's not really compelling with an example like this, frankly, that's relatively simple. It's not very complex. There's not much functionality. Honestly, the version that we just typed up-- these 10 lines-- this is fine. This solves this problem. But as our code gets longer, as we start collaborating with other people, as the problems we're trying to solve with code get more sophisticated, you're going to find that your code gets messy quickly. And you're going to find that you have a huge number of functions, for instance, in one file. And some of them are related to each other, but some of them are not. Well, at that point, wouldn't it be nice to just organize them a little differently? And in the world of Harry Potter, let's have a class for student; let's have a class for Professor; let's have a class for the sorting hat; let's have a class for something else. And so once your world gets much more complicated than some of the demonstrations we do here in class when we want to focus on individual ideas, object-oriented programming is just a way of encapsulating related data-- that is, variables-- related functionality-- that is, methods-- inside of things that have names. These things are called classes. So it's just another way to solve problems. And when we focused on libraries a couple of weeks back, that, too, was another solution to the same problem. You could define your own modules or packages, put some of your data and/or functionality in there, and that's fine, too. And sometimes which one you should use overlaps. If you're familiar with Venn diagrams, the overlapping region might mean that you could use a class; you could use a module or a package; you could just use a single local file. Over time, you'll develop an instinct and maybe even a personal preference for which tool to use. All right, let me propose, now, that we apply this same idea of a class method to clean up one other thing as well. Let me close that hat.py and reopen student.py as we left it earlier, and let me go ahead and simplify it just a little bit. I'm going to go ahead and get rid of the properties, not because there's anything, wrong with them, but just because I want us to focus on some of the key ideas when we began with this program. So I'm going to go ahead and keep main as well. I'm not going to adversarially try to change Henry's address there. I'm going to instead go ahead, though, and just print the student. But this is the thing I want to focus on here. This, in our previous student examples, was a missed opportunity to clean up my code. Well, what do I mean by that? Well, up here at the top of this file-- even though I've simplified it, but getting rid of the properties and all of that error checking-- because I want to focus on the essence of this class now-- just the student's name and the house and the printing thereof. This is, by nature of classes in object-oriented programming, theoretically, all of my student-specific functionality. That is to say, if I have functionality and data related to a student, you, the programmer, my colleague, would assume that it's all bundled up, encapsulated, so to speak, inside of the student class. And yet, if you scroll down further, what is this? There's a function called get_student that just exists elsewhere in this file that prompts the user for a name, prompts the user for a house, creates the student object, and then returns. That's not wrong. It works. And we saw many, many times it kept working. But this is a little weird because, if this is a function that helps you get a student, helps you get the name of a student and the house of a student, why isn't that functionality in the class itself? After all, as my code gets more and more complicated and does more things, I'm going to be looking at the student class for all student-related functionality. I'm not going to be scrolling down, expecting that, oh, maybe there's some other student functionality just randomly later in this file. So it's not wrong. But this is, again, evidence of maybe bad design-- not so much with this small program. But this is an example, again, of code smell. Something smells a little off here. This is probably going to get us in trouble by separating related functionality. So again it's a design principle, not a correctness concern. But class methods allow us to address this, too. Let me go ahead and do this. I'm going to delete get_student all together, leaving only main as my other function here. And inside of my student class, I'm going to do this. I'm going to define a function, even more simply called, get. And by nature of how class methods work, it's going to take in the name of the class itself or a reference thereto as an argument. And I'm going to move the functionality from get_student into the student class. And I'm going to do this-- name equals input, quote, unquote, name, house equals input, quote, unquote, house. And then what this function is going to do is return a new student object by calling class, which, again, is just an automatically passed-in reference to the class itself, passing in name and house. And I will admit this syntax seems a little strange that now I'm calling cls and I'm passing in these arguments. But let me do one final fix here. Let me go to the top of this function and more explicitly say this is a class method. This solves a potential chicken and the egg problem, so to speak, whereby one needs to come before the other, potentially. So what am I doing here? Inside of my student class, I now have a function called get. It is, I shall claim, a class method what does that mean. It just means I can call this method without instantiating a student object first. Therein lies the potential chicken and the egg problem. And if unfamiliar, that's an expression, meaning, well, and did the world have chickens first that laid eggs, or was there an egg that then yielded the chickens, but how did the egg get there? It's this weird, circular problem. And that's what we're facing here. It would be weird if you had to create a student object in order to call get, in order to get another student object. That sounds messy. Let's just get a student via a class method that, by definition, does not require you to create a student object first. Just like the hat, in its final form, we use the hat class to just say Hat, capital H, dot sort. We didn't need to create a hat first. We just used the class itself. So what am I going to do here now? Let me go down to main. And instead of saying get_student, notice what I can now do. Student.get, and everything else can stay the same. All I've done now is I've migrated all of my logic from get_student, which was this own standalone function, but clearly related to students by name. I've moved the same code, really, to inside of the student class in a more simply named function called get. But I could still call it get_student if I want. It just seems a little redundant to call it get_student in a student class, so I'm simplifying. So I have a method called get, but I'm calling it a class method to avoid that chicken and the egg problem. I want to be able to call a get without having a student object in my universe already. And the syntax for that is @classmethod. The convention is to give this method at least one argument, by convention called cls for class, which is just going to be a reference to the class itself. Lines 11 and 12 are identical to what they've always been. And get_student-- the only new syntax here is this, but this, again, is one of the features of object-oriented programming. You can now instantiate a student object by just using cls that's passed in. I technically could use Student, capital S, but it turns out I'm doing what's more conventional because this will both solve and avoid problems down the line with more complicated code. This line here, on line 13, just means create an object of the current class. What class is that? Well, whatever cls is. Well, that, by definition of how it all works, is going to be student. And I want you to initialize it, as always, with name and house. So now, scrolling down, my code is this. And this is just nice to read. You perhaps have to acquire a taste for this-- and I sound a little odd saying, this is nice to read. But indeed, student.get just tells me what's going on. I'm going to get a student. I don't need a separate function written by me called get_student in the file itself. The get functionality is built into the class. All my student-related code now is together. So let me go down to my terminal window and run Python of student.py, Enter. Let's type in Harry. Let's type in Gryffindor. And we're back to where we began. But, but, but everything related to students, now, is in this here class. The only other thing in the file is main and this conditional that we always use to avoid accidentally executing main when we're making a module or a package or the like. So again, a solution to a problem-- not a big one in the case of a relatively small program, but one that you will eventually encounter as your programs get longer and longer, with more and more entities to represent. Questions now on this use of a class method. MICHAEL: Does the class have to be defined before the main function, in terms of the order of the program? DAVID J. MALAN: A really good question. So when in doubt, let's try this. So let's try to change the order. Let's move main to the top, which I've often encouraged. So let's go ahead and, above the class, do this. And notice now that, technically, line two is mentioning student, which does not exist until line 6 and below. Let me go ahead and clear my terminal and run Python of student.py. So far, so good. Harry-- Gryffindor, OK. Indeed, Harry's from Gryffindor. The reason, Michael, it does not matter in this case is because we're not actually calling main until the very end. And just as in the past, that means that Python has a chance to read everything, top to bottom, left to right. So everything exists. I would say, generally classes are defined at the top of the file. However, it would be even maybe cleaner to move the Classes definition to its own file and then import it, so essentially to make reusable code by putting it into your own module or package so that not just this program but many others can use that definition of student as well. Other questions now on classes, class methods, or the like. AUDIENCE: I wanted to ask, is there a way to declare all the possible-- all the possible attributes of the class? Because it looks so inconsistent. DAVID J. MALAN: Well, so my takeaway there is this is Python's approach to these principles. Different languages, like Java, just take a different approach but have very similar features. The syntax just tends to vary. And this is how the Python community chose to implement this idea. The right mental model, ultimately, is that these instance variables, instant methods belong to or operate on specific objects-- a specific student, a specific hat. Class variables and class methods operate on the entire class itself or, in turn, all objects of that class, which we've not seen a demonstration of, but it's a higher level concept. So it turns out, besides these class methods, which are distinct from those instance methods, which, to be fair, do not have their own decorator-- they just are, by default, instance method, there's yet other types of Methods You can have in classes in Python. They tend to be called static methods, and they, too, come with another decorator called @static method, which is a rabbit hole we won't go down. But realize that there is yet other functionality that you can leverage within object-oriented programming. But what we thought we'd do is focus really on some final core features that you see not just in Python but other languages as well. And perhaps one of the most compelling features of object-oriented programming that we haven't yet used explicitly-- though it turns out we've seen implicitly over the past weeks-- is this notion of inheritance. It turns out, via object-oriented programming, there's actually an opportunity to design your classes in a hierarchical fashion, whereby you can have one class inherit from or borrow attributes-- that is, methods or variables from another class if they all have those in common. So what do I mean by this here? Well, let me propose that we implement, over in VS Code here, a brand new file called wizard.py. Let me go ahead and run code of wizard.py. And then let's start as before, defining a class called student. And let's go ahead and first define the underscore underscore, init method, which of course, is minimally going to take an argument traditionally called self. And in this case, let's also have it take as before a name and a house. And then in this init method, let's go ahead and assign the instance variables-- self.name = name, and self.house = house. Let's assume that there's some other functionality in this class as well-- dot, dot, dot. But let's move on now to implementing the notion of a professor in the wizarding world as well. So for this class, let's call it Professor. And a professor, let's say, is also going to have its own initialization method. So __ init. It's going to take self-- always as the first argument. A professor also has a name. So we'll pass that in second, too. And even though some professors are heads of houses, let's assume that a professor is really identified by their name and their subject area-- the class that they teach. So we'll call this third argument, subject. Now, as before, let's go ahead and assign self.name = name, and let's assign self.subject = subject here. And as before, let's assume that there's some more functionality associated with professors as well. Well, what do you notice already here in my definitions of students and professors? Typically, we're a bit reluctant to allow for any redundancy in our code. And here, I feel like my init method is taking a name for students; my init method is also taking a name for a professor; and I have these identical lines of code, like self.name = name. And this is only going to get exacerbated if I now go and add some error checking. So for instance, how about if not name, we should probably be in the habit of raising something like a value error in an explanatory message, like "Missing name." And you know what? If a professor is missing their name, I should probably copy, paste that code down here. And that's where red flags should be going off, whereby, as soon as you start copy pasting code, there's probably a better way so that we can write the code once and perhaps reuse it in some way. And here, too, object-oriented programming offers a solution. It turns out that object-oriented programming in Python also supports inheritance, whereby you can define multiple classes that somehow relate to one another. They don't need to exist in parallel in this way. There could actually be some hierarchy between them. So for instance, in the wizarding world, we could argue that both a student and a professor are, at the end of the day, Wizards. So maybe what we should really define is a third class, for instance, called wizard, that has any of the common attributes for students and professors alike. And for now, we've kept it relatively simple. The only thing they have in common is a name and a name, in student and professor, respectively. So why don't we minimally factor that out first? All right, so let me go ahead here. And just to keep things organized, at the top of my file, let's define a third class called Wizard. And a wizard will have its own initialization method. So def __init__(self), as always. And a wizard, let's say for now, is only going to be initialized with their name in this way. And now, I'm going to go ahead and do some of that error checking. So if not name will raise a value error in the wizard class. Otherwise, we'll go ahead and do self name equals name, and, heck, dot, dot, dot, maybe some other functionality as well. But not a subject, which is specific to professors, and not a house, which I've claimed is specific to students. Now, I think we can begin to maybe remove some of the redundancies in our other classes here. So for instance, down with student, why don't I go ahead and remove this error checking here and remove this error-- this assignment of self.name = name because I'm already doing that in Wizard. And similarly, down here, in Professor, why don't I do the same? Let's get rid of the error checking. Let's get rid of self.name = name because, again, I'm doing that already up there for Wizard as well. But at the moment, even though they're all in the same file, I haven't told Python that a student is a wizard and a professor is a wizard. So I really need to link these two together. And the way you can prescribe inheritance, whereby one class should inherit from another, or conversely, one class should descend from another-- we can do this. I can say class Student. But before the colon, I can go in and say in parentheses, a student inherits from, or is a subclass of wizard, which, conversely, is the superclass of the student class. So this just means that, when I define a student class, go ahead and inherit all of the characteristics of a wizard as well. And I'm going to do the same thing for Professor. So (Wizard) after the class name Professor, and that's going to give me access to some of that same functionality. But because my student class and my professor class still have their same init methods, those are the methods that are going to get called. Whenever I create a student in code or I create a professor in code, I need to somehow explicitly say that I also want to use the functionality in the Wizard class's init method. And the way to do this in Python is as follows. Let me go into my init method for Student, and let me call super, with no arguments, which is a reference to the superclass of this class. So if this class is Student, the superclass-- that is, the parent class-- is Wizard. So super() will have the effect of accessing the superclass. And then I'm going to go ahead and explicitly call its init method, and I'm going to pass to the Wizard's init method the name that the student's init method was passed. And I'm going to go ahead and do the same down here in Wizard. This is one line of copy, paste. But I think I'm OK with it here because it's still allowing me to do all of the name assignment and the error checking up in the Wizard class instead. I think we're OK now by just calling super.init for both student and Professor alike. Now, admittedly, this syntax is definitely out there-- the fact that we're calling super in parentheses and dots and underscore underscore on the left and the right of init here, but it's just a combination of these two ideas. super() is a way of programmatically accessing a current class's parent class, or superclass, and __init, of course, is just referring to, now, that class's own initialization method. Now, per the dot, dot, dot-- so there could be a lot more going on in these classes. But what's nice now is that Wizard as a class is taking care of all of the assignment of a wizard's name, whether that wizard is a student or a professor. And it's even doing some error checking to make sure the name was actually passed in. Meanwhile, student is inheriting all of that functionality and using it by calling the superclass's own init method. But it's additionally taking the house, that's presumably passed into the student constructor function, and assigning it to its own instance variable-- self.house, and similarly, professor, or restoring in self.subject the subject that was passed into that one as well. Now, how might we use these classes? Well, we'll continue to wave our hands with a little bit of detail here. But at the bottom of this file, or any other file that imports this one, I could now write code like this. I could create a student variable and assign it the return value of the student constructor call. and maybe that student is named Harry and that student's house, for instance, might be Gryffindor. And meanwhile, I might do something like this. professor = Professor over here. And notice, the lowercase S on the left, capital S on the right. Same for professor on the left-- lowercase and uppercase on the right respectively. Professor, quote, unquote, "Severus," and how about Defense Against the Dark Arts will be his subject? And meanwhile, if we want, more generically, just a wizard, who, at the moment is neither student nor professor teaching classes actively, we could even do that. We could do wizard = Wizard in capital W on the right-hand side of the equal sign, because it's the name of the class. And someone like Albus-- passing in only Albus's name-- not a house, not a subject, because, in this case, he's known only as a wizard. Meanwhile, with each of these calls, this line of code here will ensure that the init method for the wizard class is called. This line of code here will ensure that the init method of the student class and, in turn, the init method of the superclass wizard is called. And then lastly, on this final line of code, will this syntax ensure that the init method of the professor class is called, which, in turn, calls the init method of the superclass as well. Any questions now on this idea of inheritance, which is a key feature of a lot of object-oriented programming languages? MICHAEL: From what I've seen so far, a lot of times, there's a lot of nesting. If you do super, does it go one up? Is there any situation where it's nested in another class as well, above Wizard, let's say? DAVID J. MALAN: A really good question. If you were to have a super superclass-- so your hierarchy is even taller than the two levels of hierarchy that we currently have, absolutely. What's nice about inheritance, as the name implies, is, just as you might have inherited certain traits as a human from your grandfather and grandmother or your great-grandfather or great-grandmother, some of those properties can actually trickle down to you in the context of code as well. So when you descend from another class-- that is, when you subclass a superclass or a super superclass, you actually do inherit all of the functionality, not just from one level above you but from two or three, so you can indeed access some of that functionality as well. And you can even override it if you want some of these classes to behave a little bit differently than others. Other questions on inheritance. AUDIENCE: So it's similar to the last one, but can you have two parents on the same level? DAVID J. MALAN: A really good question. So there are ways to implement descendants from multiple parents. And there's different ways to do this, not just in Python but other languages. We've kept things simple here, though, by having a single inheritance path. A good question. How about one more question on inheritance? AUDIENCE: Can we have multiple arguments in super.__init? DAVID J. MALAN: Yes, but in this case, I'm only passing a name on line 18, and I'm only passing in name on line 10. Why? Because, on line 2, when I define the init method for the Wizard class, I only expect a single argument. But I could absolutely have other common functionality. I could add in a patronus. If both students and professors have patronuses that can come out of their wands, I could have two arguments instead. We've been using this feature of object-oriented programming now for quite some time in the form of exceptions. Indeed, if you look at the official documentation for exceptions in Python, you'll see that there's not even the ones we've seen in class, like value error and others. There's any number of others as well, but they are all, themselves, hierarchical in nature. This is just a subset of the available exceptions that come built into Python. And you can actually, as a programmer, create your own exceptions as well. But as this chart here captures hierarchically, all exceptions we've seen thus far actually descend from or inherit from superclasses already. So for instance, at the bottom of this list here is ValueError, which we've seen quite a bit. And if you follow the line straight up on this ascii rendition of this chart, you'll see that ValueError has a parent, class, or superclass, called exception. And the exception class, meanwhile, has a parent class called base exception. Why did the authors of Python do this? Well, it turns out that, whether you have a value error or a key error or an assertion error or any number of others, there's a lot of functionality common to all of those types of errors that you want-- that you want a programmer to be able to use. And so it turns out that the authors of Python decided, you know what? Let's not have a dozen or more different classes that all just have copy, pasted similar functionality. Let's create this hierarchy so that, even though the exceptions toward the bottom of this list are very precise, they at least inherit-- that is, borrow some very common functionality up above. So it turns out that, when you use the Try and the Accept keyword in Python, generally speaking, we've tried to catch very specific exceptions, like ValueError. But technically, you could capture the parents or even the grandparent exception for a given exception, especially if you're not necessarily sure which one is going to get raised. Or, better yet, there could be many exceptions that get raised, but you want to handle them all the same, and you don't want to necessarily enumerate them in parentheses, separated by commas. You want to say you want to handle all exceptions of a certain superclass in much the same way. So this has been latent this whole time, any time we've seen or used or caught or, now, raised exceptions, and built into Python is this hierarchy. And if you were to invent your own exception, generally, you wouldn't want to start from scratch. You would want to descend from-- that is, subclass, one of these existing exceptions and add your own twist on it, your own functionality as well. Well, there's one final feature of object oriented programming that we'd like to share with you today, and then it will perhaps be quite the eye opener as to what you can really do now that you have classes at your disposal. And this, too, surprise, has been a feature you and I have been taking for granted for weeks now. This has just worked, but it's been implemented in a way that you can now leverage yourself. It turns out that Python, and some other languages, too, support this notion of operator overloading, whereby you can take very common symbols, like plus or minus or other such syntax on the keyboard, and you can implement your own interpretation thereof. Plus does not have to equal addition. And minus does not have to equal subtraction. And in fact, you and I have already seen another context in which plus means something else. Plus has not always, in Python, meant addition, per se. What else has Python used plus for? AUDIENCE: Concatenation? DAVID J. MALAN: For concatenation. For joining two strings, for adding to a list can you use plus as well. So plus has actually been, funny enough, overloaded by the authors of Python for us. And so we can use the same symbol in much the same way as addition but with different data types to solve slightly different problems. Well, let me propose that we go back over to VS Code here, and let me go ahead and create a new final file called vault.py. So code of vault.py. And let me propose that we implement the idea of a vault at Gringotts, keeping on theme, wherein there's a bank in the world of Harry Potter. And within this bank, families and individuals have vaults containing all sorts of money in the wizarding world. And the type of money that exists in the world of Harry Potter are coins called galleons and sickles and Knuts, and those are in descending order of value. And so inside of a vault might be a whole bunch of coins-- gold, silver, and bronze, essentially, each in those denominations, tucked away. So how can I go about implementing, first of all, the idea of a vault so that I can store, for instance, for Harry Potter, how much coinage is in his family's vault, or for Ron Weasley the same? Well, let me go ahead and vault.py and first create a class called Vault, essentially meant to represent a bank vault. Perfect, another real world, or fantasy world, entity that I want to represent with code. I could use a tuple or a list or a dictionary. But again, I'm going to get a lot more functionality with classes, and we'll see one final flourish with operators. Inside of this vault class, let's go ahead and do this. Let me define my init method, taking its first argument of self. And let me define three arguments to this. When you create a vault, in my code here, I want to be able to initialize it with some number of galleons, some number of sickles and, some number of Knuts. I want the user, the programmer, to be able to pass in one or more of those values ideally. But they can be optional, so I'll give them defaults. So let's go ahead and define a parameter called galleons, whose default value will be 0; sickles, whose default value will also be 0; and knuts, whose default value will be 0 as well. So the programmer can pass in one or two or three or even none of those, and they'll all have some implied defaults. How do I want to remember those values that are passed in? Well, let me do this. self.galleons = galleons. And self.sickles = sickles. And self.knuts = knuts. And so I could add some error checking, especially if you don't pass in. A number I could turn these into properties to do even more validation. But let's keep it simple and, as always, focus only on the new ideas. So I'm just going to trust that these values were passed in, and I'm going to immediately assign them to these instance variables. What, now, do I want to do? Well, let's come up with a way of printing out what is in someone's vault, ultimately. But first let's do this. Let's create a vault for the Potters by creating, via assignment, a new vault. And let's say that the potters have 100 galleons, 50 sickles, and 24 knuts. And that's in that vault. And let's print out, for instance, potter. All right, let's run this code and see how it works now. Let me go ahead and run Python of vault.py, Enter. Seems to work. No syntax errors or anything else. But this is not very enlightening. How do I fix this, thinking back to what we've done before? AUDIENCE: You have to use the __str. DAVID J. MALAN: Exactly. I need to use one of those special methods that comes with classes and define for myself how I want a vault to be printed as a string. So let me go ahead and do that. Let me define the str method taking in self as its sole argument here. And let's just return a very simple string that just reveals what's in the vault. So I'm going to return a formatted f string, inside of which is self.galleons and then the word galleon, so I know which those are. Then let's do self.sickles, and let's output the word, sickles. And then lastly let's output self.knuts, and then knuts here. So I know, in this string, just how many of each of those coins I have in this particular family's vault. All right, let me go ahead and run Python of vault.py, changing nothing else except the str method. And now, , we, see indeed that Harry has 100 galleons, 50 sickles, and 25 knuts. All right, well, let's do one thing more here. Below that, let's go ahead and define a Weasley variable. And Ron never seemed to have quite as much money in the vault as did Harry. So let's say that the Weasley vault will have 25, 50, and 100. So I'll just reverse the order of those denominations, rather than Harry's 100, 50, 25. And now let me go ahead and print Weasley like this. And let's go ahead and clear my terminal window, run Python of vault.py. This time, that str method will be invoked twice, once for each of those vault objects. And we'll see, indeed, that the first one for Harry has got 100, 50, and 25, respectively, versus Ron's 25, 50, and 100, respectively. But now let's do something interesting. Suppose that you wanted to combine the contents of two vaults, be it Harry's and Ron's or any other two people. How would you go about doing this in code? Well, if I wanted to combine the vaults for someone, I could do this. Well, I could do galleons equals-- let's do potter.galleons + weasley.galleons. That gives me a variable called galleons that has the sum of Harry and Ron's galleons. Let's next do sickles = potter.sickles + weasley.sickles. And then lastly, let's do knuts = potter.knuts + weasley.knuts. I've got three variables. What can I now do with these values? Well, let's create a third-- a new vault. Total will be the name of this variable equals a new vault, Capital V, notice. And now, let's pass in those three new variables-- galleons, sickles, and knuts. And that's it, and let's print out this total vault. So we should now see three vaults-- one for Harry, for Ron, and the combination-- the addition of the two. Let me go ahead and rerun Python of vault.py, and there we have it. What was 100, 50, 25 and 25, 50, and 100, combined through addition now, is 125, 100, 125. So pretty straightforward, using techniques from weeks ago, where we're just declaring a few new variables and doing some addition. But wouldn't it be cool if I could do something like this? Wouldn't it be cool if I could just somehow, not manually create my own vault and do all of this annoying math up here-- what if I could just do potter + weasley and get rid of all of this logic here? Wouldn't it be nice if I overload the operator-- we know as plus, just like str does, just like list does-- to allow me to add two vaults together on the left and the right. Well, it turns out in Python and through operator overloading, there is a way to do just this. If you consult the documentation, there's this and so many other special methods that come with classes. The third one we'll see here is this one here-- __add__. And you'll see that it very generically is described in the documentation is working for any object, be it a vault or str or a list or something else. By convention, it's going to take a first argument called self, and then it's going to take some other argument, by convention, called other. self, in effect, is going to be referring to whatever object is on the left of a plus sign. other is going to be referring to whatever is on the right-hand side of a plus sign, thereby giving us a way of describing, in code, the operand on the left and the operand on the right of the operator, plus, in between. That is to say, if I go back to VS Code here, what I'm trying to do is implement support for this. Well, let me try, without writing any other code just yet-- Python of vault.py, Enter-- TypeError: unsupported operand type(s) for +: 'Vault' and 'vault.' That is to say Python, at this moment, does not know what it means to add two vaults together. You and I might have an instinct. Probably want to combine the galleons and the sickles and the knuts respectively. Python doesn't know that. It just knows that you have a new class called Vault. But let's teach Python to do this. Let me clear my terminal window. Let me scroll back up to the class itself, where, at the moment, I only have two special methods-- init and str. But let's add this third. Let me go into the class here and define __add__ and then specify its first parameter as self, as before, and then a second parameter for this particular method called, by convention, other. Now, as always, I could name those parameters anything I want, but I'm going to stick with convention here. And now, inside of this method, am I going to have to now add together the contents of two vaults? Well, what two vaults? Well, if we scroll down to our goal at hand, the goal, of course, is to add this vault plus this other vault-- potter plus weasley, respectively. Well, it turns out, in Python, that, when you do overload an operator like plus, what's going to happen automatically, as soon as Python sees that, is it's going to call that __add__ method, and it's going to pass into it to arguments-- whatever the operand is on the left-- potter, in this case-- and whatever the operand is on the right-- weasley, in this case. And those values are going to get passed in as self and other, respectively. What that means is that we can access their contents up here in our implementation of add as follows. Let me go ahead and define a local variable called galleons and set that equal to, for instance, the sum of self.galleons-- whatever's in Potter's vault in this case, plus whatever is in Wesley's vault in this case, which would be other.galleons. Let me do the same for sickles. self.sickles + other.sickles. And let me lastly do that for knuts. So self.knuts + other.knuts. But at the end of the day, I'm going to need to return a brand new bigger vault that contains all of those contents together. And if we ultimately want to assign that bigger vault to a variable like total here, on the left, we'd better return a value from this add method. So I'm going to go ahead and give myself a brand new vault, as by returning capital Vault, which of course, is going to call my vault function into which I can now pass some of those initialization arguments. Well, how many galleon, sickles, and knuts do I want this brand new vault to contain? Well, I want it to contain this many galleons this many sickles, and this many knuts. So ultimately, what we're doing in this implementation of add is adding together those galleons, sickles, and knuts, passing them to the vault function so that we get a brand new bigger vault, and return that altogether. So now I've defined this new special method called add that should now just make plus work for two vaults. Let's see. Let me run down to my terminal window, Python of vault.py and hit Enter. And voila, and now we've implemented an overloaded operator, plus, to do what you and I as humans would hope would be the case when you add two vaults together. But I've now written the code more specifically to teach Python what it means concretely to add two vaults together. And it's with very similar code in effect, underneath the hood, that Python is doing this for two strings, to concatenate them together, to joining two lists into a new list with list, and so many other classes as well. Any questions now on operator overloading or this example here. AUDIENCE: How would you go about creating a function for adding a student and a vault for two separate classes? Would that be possible? DAVID J. MALAN: Let me see what happens here. I don't know offhand. Let's do this. Let's create a str and see what happens. If I add Potter plus a str-- str object. Yeah, so it would work. I'm just figuring this out as I go here, Eric. So just to be clear, what I did was I just changed weasley to str just to see what would happen when I add a vault plus a str, and it will work, theoretically. Why? Because so long as the type of value on the left has an add method implemented, other can be any type that you want. You just have to decide and code what it's going to mean conceptually to add a vault plus a string, which, in this case, probably doesn't make any sense at all, but it's possible. It's going to be the operand on the left. And I'm inferring that. I did not know the answer a moment ago. I'm inferring that because what I got was an attribute error here on line 11 because Python did not like this. other.galleons didn't work, but I could make it work by figuring something out. Really good question. Didn't know that one myself. Other questions on operator overloading? AUDIENCE: Can you define new operators in Python? DAVID J. MALAN: I don't think so. There is a very long but precise list of operators that you can overload. I do not believe you can assign arbitrary characters to be operators in Python. Let me defer to Carter in the chat to-- OK, I'm seeing two of my colleagues are saying, no, not possible. So I'm going to go with my first instinct, no. Otherwise, that'd be kind of cool. You could make emoji do whatever you want to. How about one final question on operator overloading? AUDIENCE: Is that the only operation you can do as far as-- can you do a subtraction as well? DAVID J. MALAN: You can. You can do so many others let me. If, Carter, you don't mind pulling up this URL here-- so this link here-- special method names and today's slides, you'll see a long list of all of the operators that you can overload. You can do less than, equals than, plus equals, minus equals. Pretty much any symbol you've seen me type on the screen can be overloaded in the context of classes. So even though, today, we focused entirely on object-oriented programming, this is a technique that we've been using, really, since the first week of the class because those ints, those strs, those floats, those lists, those dictionaries, and so much more were already underneath the hood this whole time-- classes and objects thereof. But you now, as a programmer, have the ability to create your own classes with your own instance or class variables, with your own instance or class methods, with your own properties, and even with your own custom behavior for operators. So ultimately, you can absolutely continue using those simple tuples or lists or those dictionaries or other structures as well. But object-oriented programming, and with it, classes and now these objects is just another tool in your toolkit. And daresay, as your code gets more sophisticated and your problems get bigger, you'll find that being able to model these real world or even fantasy world entities with classes and related data and functionality will ultimately just allow you to define code that's not just correct but ever well-designed as well. This was CS50.