 Finally, let's look at how to actually create your own classes. Classes in Python are created with class statements. Class statements superficially look a bit like function definitions. Instead of def, you have the reserved word class, and then you put the name to which the newly created class object will be assigned. Here the name is jack. And then following that in parentheses, rather than an argument list, we put simply an expression which returns a class. This becomes the parent of the new class we are creating. So here in the parentheses, we have an expression of variable kate, presumably referring to the kate class. And then after the parentheses, the header ends with a colon, and what follows is a body of code. This body of code gets executed once when the class is created. So be very clear on that. When you define a function, the body of the function is not executed when the function is created. It's only executed when the function is invoked, and that may happen many times. Here the body of the class executes when the class object itself is created. And that only happens the time the class statement itself executes. Now the body in this class, what happens is it can contain any code we want, but any names we assign to in the class body get added as attributes to the class object. So in this class statement, a class object is being created, which inherits from kate. That class object has two attributes, one x with the value of three, and the other named whale with a function object. And once created, that class object itself is assigned to the variable jack. Notice that the function here, whale, its first parameter is called self. That's a convention of Python classes. Because the function in this class is going to be effectively a method, the instance object should normally get passed in as the first argument to the function. And the convention in Python is to refer to the instance and methods as self. If you create a class and don't specify a parent class, implicitly the new class will inherit from object. Actually in these cases, you don't have to write the parentheses at all, we can simply omit them. In any case, now that we have our class object, we can create instance of this class by simply invoking it. But now, what about constructors, and what about giving our instances their own fields? A class in Python is effectively given a constructor by giving it a method with a special name, init, init with double underscores. When we create a new instance, Python will implicitly invoke this function as a method on the new instance. So now in this example, when we instantiate jack, the new jack object gets passed to self in the init function. And so when four is assigned to the attribute foo of self, that's giving the new instance an attribute foo with the value four. That's how in Python, we get constructors in fields. The init function acts as the constructor, and in that constructor, you can give the instance fields by assigning it attributes. Now for many classes, we want the constructor to effectively be parameterized. We want to supply input when we create instances. You can have this in a Python class if we simply give init more than one parameter. And then when we instantiate the class, we must provide arguments to pass to these parameters. So here, when the init function has parameters a and b in addition to the first one self, and we instantiate jack with the arguments a and four, well, per usual, the new jack instance is passed to self, but eight here is passed to a and four is passed to b. So when we add a and b, we get 12. And so 12 is assigned to the attribute foo of the new jack object. Now in the parentheses where we specify the parent class, we can actually specify more than one because Python supports multiple inheritance. It's possible for a single class to have more than one direct parent. Here the class, Aaron has two parents, Natalie and Jack, and notice they're separated by commas. The question this raises is how does it affect attribute searches? Attribute searches are the essence of the inheritance relationship in Python. Well the rule for this turns out to be slightly complicated. So I'll give you just the brief version which is that the search is done breadth first rather than depth. So in this example, the attribute search goes from Aaron to Natalie and then to Jack and then to the parent of Natalie Simon and then to the parent of Jack Kate before finally going to object there at the top of the hierarchy. The alternative to this search behavior is if Python were to do depth first rather than breadth first. In a depth first search, Python would check Natalie and all of its ancestors before would even look at Jack and its ancestors. Actually depth first is how Python did this search in earlier versions. In Python 3 however, it's always breadth first. And lastly do note that it matters the order in which we write the classes in the parentheses. So here because Natalie is listed before Jack, the attribute search checks Natalie before it checks Jack. Fortunately in practice, multiple inheritance just isn't used all that often. One reason being that it's simply just kind of confusing. The other reason being that generally it's not all that useful. However, Python unlike some other languages does allow for multiple inheritance. Each file of Python source code is called a module. For every module which is run, the Python interpreter creates an object to represent that module and as that module executes line by line, any names that get assigned at the top level of the module, that is any assignment which is not inside a depth statement or class statement in that module, those assigned names get added as attributes to the module object. So once a module finishes executing, we're left with this module object that contains attributes for everything that was assigned in that module. This is how you can have multiple modules tied together. Within one module, if we want to invoke say a function defined in another, we just need to access it as an attribute from the module object representing that other module. Consider this example. Say we have two files of Python source code, one called April.py, the other called June.py. In the April module, two names get assigned to at the top level. First a variable x is given the value 3 and then the depth statement creates a function object which it assigns to the variable foo. So the April module object ends up with two attributes x and foo. Over in the June module, we can access the April module by importing it with an import statement, which we'll explain more in detail later, but in short what happens with the import statement here is first Python looks for a module called April. If that module has not yet run, Python will execute that module and create an object module for it and then back in June, the import statement is implicitly an assignment. In the module June, the name April is being assigned the April module object. So then in the next line, when we write April.x, that returns 3 and if we write April.foo, that returns the function foo defined in April. If however we write April.y, well the April module has no y attribute, so this will throw an exception. When we start the Python interpreter, we need to specify one module for the interpreter to run and that module runs and in course of doing so it may import other modules thereby causing them to run. It may happen though that a single module needs to get imported by multiple other modules and so in the course of running, a single module may end up imported more than once. When this happens though, the interpreter will only actually execute the module the first time it is imported. For all subsequent imports, that import simply uses the existing module object. So at first it may seem that a module is sort of like a function, it's this piece of invocable code, but it's really not like a function because it's guaranteed by Python only to run once in the course of that program. And also of course, unlike functions, modules don't have parameters and they don't return a value when they're done executing.