A Comprehensive Guide for Classes in Python | by Soner Yıldırım | Towards Data Science

We see a bunch of different clocks in this photo. They have different shapes, colors, and sizes. However, all of them are a type of clock. We can think of classes in Python in a similar way. A class represents a type (clock) and we can create many instances of that type (clocks in the photo above).
Object oriented programming (OOP) paradigm is built around the idea of having objects that belong to a particular type. In a sense, the type is what explains us the object.
The explanation of an object is of crucial importance for OOP. We need to have a comprehensive understanding of:
What an object represents
What kind of data the object stores
How we can interact with the object
How we can implement the object in our code
All these points that constitute the explanation of an object are defined with classes. Everything in Python is an object of a type such as integers, lists, dictionaries, functions and so on. We define a type of object using classes.
In this article, we will go over what class means, how a class is created and used in Python, and what kind of advantages we obtain by using classes. The outline is as follows:
What is a class in Python?
Creating a class
Defining class methods
Class vs instance variables
Creating a child class
Classes possess the following information:
Data attributes: What is needed to create an instance of a class
Methods (i.e. procedural attributes): How we interact with instances of a class.
There are many advantages of using classes. Instead of listing all at once, I will mention them throughout the article.
We use classes in Python all the time. For instance, when we create a list, we create an instance of type list.
words = ['data', 'science', 'machine', 'learning']
We are not actually interested in how the list class is created. We just need to know how to interact with lists and use them efficiently in our code. This is the idea of abstraction.
For instance, we can remove an item from a list using the remove method.
words.remove('data')print(words)['science', 'machine', 'learning']
The following code creates a class called Book.
class Book():  def __init__(self, name, writer, word_length):    self.name = name    self.writer = writer    self.word_length = word_length
The __init__ is a special function that is automatically executed when an instance of class is created. It is also called class constructor.
The parameters of the init function represent the data attributes of a class. Thus, if we need to specify the arguments for name, writer, and length parameters to create an instance of Book.
Note: Self refers to the instance itself. You can use any word instead of “self” but it is a highly common practice to use “self”.
Let’s create an instance.
b1 = Book("Pandas", "John Doe", 100000)print(type(b1))<class '__main__.Book'>
b1 is an object that belongs to the Book class. We can confirm it by using the type function which returns the type of an object.
We can access or modify the attributes of a class using the following way.
print(b1.name)Pandasb1.name = 'NumPy' #updates the name attributeprint(b1.name)NumPy
The Book class only have data attributes. We should add methods (i.e. procedural attributes) to make it for useful and functional.
For instance, we can implement a method that returns the number of pages given the fontsize. We specify the length of the book in number of words. The method will calculate the number of pages based on the length and fontsize.
def number_of_pages(self, fontsize=12):  word_length = self.word_length  if fontsize == 12:    words_in_page = 300  else:    words_in_page = 300 - (fontsize - 12) * 10  return round(word_length / words_in_page)
We add the number_of_pages in the class definition. It calculates the number of pages of a book based on the number of words and fontsize.
If a function we declare inside a class definition needs to access data attributes of an instance, we need to tell the function how to access them. This is what we did in the first line of the number_of_pages function.
We can access a method from the class or the instance. Here is a simple example that demonstrates both ways.
b1 = Book("Pandas", "John Doe", 100000)b1.number_of_pages()333Book.number_of_pages(b1)333
The number_of_pages function has an additional fontsize parameter. Since a default value (12) is specified, we do not have to explicitly write it. However, we can use a different value for the fontsize parameter.
b1.number_of_pages(14)357b1.number_of_pages(fontsize=16)385
As the fontsize increases, the number of pages also increases which makes sense.
There are certain methods that we need to define for our class in order to use some built-in functions of Python. Consider the print function.
print(b1)<__main__.Book object at 0x7fa4cf9f7588>
The print function returns the type and the memory location of the object by default. However, we can customize its behavior by implementing the __str__ method in our class.
def __str__(self):  return "<" + self.name + ", by " + self.writer + ">"
We add the __str__ method in our class definition as above. Here is how the print function works for our class now:
print(b1)<Pandas, by John Doe>
Class variables are declared inside a class but outside of any function. Instance variables are declared inside the constructor which is the __init__ method.
The class variables are more general and likely to apply all of the instances of a class. On the other hand, instance variables are more specific and defined for each instance separately. Having a distinction between class and instance variables is quite useful.
Consider the Book class we defined earlier. We run a publishing company and have some standards for the books we publish such as page width and color for the cover. If we define them as class variables, we do not have to explicitly declare for each instance created.
class Book():  page_width = 14  cover_color = "blue"  def __init__(self, name, writer, word_length):    self.name = name    self.writer = writer    self.word_length = word_length
We have implemented the page_width and cover_color as class variables because they are inside the class definition but outside any function definition.
Let’s create an instance of the Book class.
b2 = Book("Machine Learning", "Jane Doe", 120000)
We have not specified the class variables while creating this instance. However, the b2 possesses these variables and we can access them.
b2.page_width14b2.cover_color'blue'
We have the option to change the class variables for a particular instance.
b2.cover_color = 'red'b2.cover_color'red'
The changes on a particular instance do not have any effect on the class variable.
Book.cover_color'blue'
We can create a class based on a different class. Let’s create a class called “ColorBook” based on the “Book” class.
class ColorBook(Book):
The ColorBook is a child class of the Book class. When we create a class in this way, the child class copies the attributes (both data and procedural) from the parent class. This concept is called inheritance which makes the OOP more efficient and powerful.
It is similar to the inheritance in real life. Most of our genome come from our parents or ancestors. We inherit from them. Thus, we have similarities with our parents.
A child class can have new attributes in addition to the ones inherited from the parent class. Furthermore, we have the option to modify or override the inherited attributes.
Let’s define the __init__ function of the ColorBook class. It will have two additional parameters which are “color” indication the color of pages and “has_image” indicating if there are images in the book.
class ColorBook(Book):  def __init__(self, name, writer, word_length, color, has_image):    Book.__init__(self, name, writer, word_length    self.color = color    self.has_image = has_image
Since the name, writer, and word_length have already been defined in the Book class, we can just copy the __init__ method from it. We just need to define the additional attributes.
Note: We are free to define each data attribute manually for the child class. Using the __init__ of parent is optional.
Let’s create an instance of the ColorBook class.
c1 = ColorBook("Seaborn", "John Doe", 90000, "green", True)c1.name"Seaborn"c1.color"green"
The child class also inherits the class variables.
c1.cover_color"blue"c1.page_width14
The methods are also copied from the parent class. For the Book class, we defined two methods that can also be used on the instances of the ColorBook class.
c1.number_of_pages()300print(c1)<Seaborn, by John Doe>
We have the option to override the data and procedural attributes (i.e. methods) inherited from the parent class. This makes the inheritance even more powerful because we are obligated to use everything in the parent class.
For instance, we can modify the __str__ method for the ColorBook class.
def __str__(self):  return "<" + self.name + ", in " + self.color + ">"
The print function will return the name of the book and its color.
c1 = ColorBook("Seaborn", "John Doe", 90000, "green", True)print(c1)<Seaborn, in green>
Here are the class definitions for the Book and ColorBook classes.
What we have covered in this article can be considered as a comprehensive introduction to Python classes. We have mentioned the importance of classes for object oriented programming and how classes demonstrates the key concepts such as abstraction and inheritance.
For the practical side, we have defined two classes and seen how inheritance works when creating a child class.
There is much more to cover about Python classes. Once you are comfortable working with the basics, feel free to move onto the more advance topics.
Thank you for reading. Please let me know if you have any feedback.