A Primer on Object Oriented Programming with Python.
Tags: python, oop, programming, computerscience
Published: 2021-10-27 08:47:20

## Article Agenda
This article is focused at getting you up and running pretty quick on the concept of **OOPs in Python**. More specifically, we will cover the following topics:
- [**Classes**](#class-in-python-oops)
- [**Instances**](#instance-in-python-oops)
- [**Methods**](#method-in-python-oops)

This article is going to be a quite a bit lengthy since I will be giving you a proper explanation to each concepts that I am going to discuss. But I hope you will have a lot to take away form this.

###### There is another interesting follow up topic to OOP in Python, which is called **inheritance**. This is a whole world in itself and for the sake of not converting this article into a full-fledged book, I decided to talk about it in another article. It deserves a dedicated article on its own.

## Introduction
![Introduction](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/18g6prd16wt9ew21lbo6.png)
Python has been there in the computer science realm for a long time. Before Python, JAVA used to be the **Big Thing** (It is still very popular, but not that much when compared to Python).

One of the most desirable features of Python is Object Oriented Programming (OOP in short). OOP in Python is pretty neat and very organized. Developers really **love the flow** while writing Python OOP.

So, with that being said, lets quickly jump right in.

## Object Oriented Programming
In object-oriented programming, I realized the power of using classes and objects as a paradigm for writing codes. In fact, OOPs, as it is called, is all about using **classes and objects**. One thing I noticed while using Python’s OOPs is that, its quite different from other programming languages like C, C++. **Python’s OOPs is crisp and straight to the point**.

By definition:

> Object-oriented programming (OOP) is a method of structuring a program by bundling related properties and behaviors into individual objects


Conceptually, objects are like the **components of a system**. Think of a program as a factory assembly line of sorts. At each step of the assembly line a system component processes some material, ultimately transforming raw material into a finished product. An object contains data, like the raw or pre-processed materials at each step on an assembly line, and behavior, like the action each assembly line component performs.

## Class in Python OOPs
![class](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yekqkt8j8t8vg0atw8rl.png)
To understand the concept of class, lets take an example. Suppose in **_LPU_** (a university), there are hundreds of professors. **Each professors have their own credentials like name, age, id etc**. What sort of data type will you use to store such a vast data and manage them in a meaningful way? One quick workaround would be to use a list to represent them. For example:

```python
ishaan = ['Ishaan', 125, 23]
loviRaj = ['Lovi Raj', 225, 34]
pooja = ['Pooja Rana', 34, 25]
# nameofProfessor = [‘name’, age, id]
```

But this mode of representation has a **big limitation**. You’d need to create separate variables for all the professors. Also, lets suppose that a newly joined professor say, **Pratheek** has not been assigned an id yet. Now, if you are managing a really big data like this one, you might not even realize that **Pratheek** has **no id yet**. So, generally, if you try to retrieve **Pratheek’s id**, you might end up getting **his age** as shown below:

```python
shaan = ['Ishaan', 125, 23]
loviRaj = ['Lovi Raj', 225, 34]
pooja = ['Pooja Rana', 34, 25]
pratheek = ['Pratheek', 19] # id has not been given yet

#retrieving ids
pooja[1] # will give you pooja's id
pratheek[1] # will give you pratheek's AGE (but needed his id)
```

This is some sort of inconsistency. Therefore, we need a better way to handle such **real-world data**. Here comes the class.

Creating class is easy. It is shown below:
```python
class Myclass:
    pass
```
Now, to solve our above problem using classes, first of all, we would have to declare a class with a useful name, say **Professor**. Each class in python has its own attributes. These attributes are either the **instance variables or the class variables.**

```python
class Professor:
    pass

```
It’s that simple. You have a class named Professor. Now, we will currently leave class as empty. I will fill this once I finish talking about the instances.

## Instance (Or Object) in Python OOPs
Instances are **ways through which one can assign** or **retrieve information** from the class or perhaps even **modify it**. **Every instance in class is unique** having **their own blueprints of the class**. So, if I create three instances of **Professor** class, I would have blueprints of Professor’s class in **each of these three instances and all the three instances** are **independent** of each other.

The syntax for instantiation is:
> instanceName = className(inheritSomething[if any])

Let’s see this in action. In the following code, I have instantiated the class with an instance called **prof**. Then, I have given the **attributes (name, id, age)** of the class by using the instance **prof**.
```python
class Professor:
    pass
prof = Professor() # instantiated the class with prof instance

# using prof instance to give class attributes
prof.name = "ProfessorName" 
prof.id = 123
prof.age = 45
```
So, with this, we have **successfully registered an object** of Professor class. Now, lets take a look at the previous case where **we don’t have Pratheek’s id**. So, in that case you don’t even have to pass the **_id_ attribute**. Therefore, **when the developer tries to retrieve Pratheek’s id**, he would be **thrown an error** instead of giving him the **age of Pratheek which is absolutely good since it did not fetch us the wrong value**. The below code demonstrates this scenario:
```python
class Professor:
    pass

#instances
pooja = Professor()
pratheek = Professor()

#assigning attributes
pooja.name = "Pooja Rana"
pooja.id = 34
pooja.age = 25

pratheek.name = "Pratheek"
pratheek.age = 19

#retrieving the ids
pooja.id                      # no error
pratheek.id                   # error
```
In the previous **no-class** scenario, we could have got an ambiguous value (mismatch of _age_ and _id_), which would have gotten us into some serious troubles. **Also, by using class, the code got a meaningful structure**. When you want to add a new attribute, just use an object and give an appropriate attribute name and you are done! That’s why Oops is really interesting to work with. 

Ok, so one thing I noticed in the above way of assigning attributes to instances is that, we would have to use the **_dot_** and **attribute name every time we would want to give a value**. This **might get cumbersome** if you are having really large number of professors in the university. Also, **this is a repetitive task**. Notice how **_‘.name, .id, .age’_** **is repeated for all the instances?** Well, there is a neat way to circumvent this. It is, by using **_init() method_**. But before moving onto **_init method_**, lets talk about **methods** for a moment.

## Method in Python OOPs
![Methods](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hrghzdxcnyh8q57cd2lp.png)

Just like we make use of functions in Python, we can have functions inside class as well. **Any function that you define inside class is no longer called a function, it’s called a method. Every method inside a class must’ve it’s first argument as _self_.** There is a reason to it. Which I will be demonstrating in the upcoming code snippet. We can call **the function from outside the class using the instance name**. The following code snippet demonstrates the methods concept of Python:

```python
class Professor:
    def displayName(self):
        print(self.name)

#instances
pooja = Professor()
pratheek = Professor()

# assigning attributes
pooja.name = "Pooja"
pratheek.name = "Pratheek"

# calling method
pooja.displayName()       # Pooja
pratheek.displayName()    # Pratheek
```
Here, notice how the displayName gave different outputs for both **Pratheek and Pooja**. Well, that’s because as I mentioned earlier in this report that, whenever you create an instance, it gets an independent blueprint of the class. Therefore, **the attributes of Pratheek are in no way having control over Pooja’s**. Hence, we got two outputs.

Okay, lets talk about that **_self_** keyword. Now, **by default, whenever you call a method,** in the backend, python **does this conversation** implicitly: 

```python
className.methodName(instanceVar)
```

Therefore, **_pooja.displayName()_** is converted implicitly to:
```python
Professor.displayName(pooja)
```
So, as you can see that **automatically, python passes the instance as its first argument**. Therefore, to receive this argument, we use the keyword  **_self_**, although you can use **any word of your choice.**

Now, let me take you back to the **_init method_**. Init method is **used to initialize attributes to a given object**. So, instead of writing **_object dot attributename_** every time, we can pass 
those **attributes values as the parameters to the class name when we declare an instance**. 
The following code demonstrates the usage of **_init method_**:
```python
class Professor:
    def __init__(self, name, id, age):
        self.name= name
        self.age = age
        self.id = id

    def displayName(self):
        print(self.name)

#instances
pooja = Professor('Pooja', 34, 25)
pratheek = Professor('Pratheek',None, 19)

#calling methods
pooja.displayName()        # Pooja
pratheek.displayName()     # Pratheek

```
The code looks **more organized now**. See how I’ve passed the attribute values while I declared the object. Also, **_id_ has become a mandatory field now**. Since Pratheek doesn’t have an id yet, I assigned **_None_** as its **_id_**.

In python, we do have other method types apart from these normal methods and they are:
**`class methods`** and **`static methods`**

### Methods vs Class Methods vs Static Methods
![ClassvsStatic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bztxf27ucvenx0wsis9y.png)
In this sub section, we will be covering class and static methods. Now, before starting off with these, its imperative to understand the difference between **class and instance variables**. Class variable is **shared across all the instances** while **instance variables are specific** and is unique for that instance only. Class variables are **declared inside class and outside any methods**. Instance variables are **declared inside _init method_**. The following code snippet shows class and instance variables:

```python
class Professor:
    classvar = "this is a class variable" # class variable

    def __init__(self, name, id, age):
        self.name = name      # instance variable
        self.age  = age       # instance variable
        self.id   = id        # instance variable

    def displayName(self):
        print(self.name)

#instances
pooja = Professor('Pooja', 34, 25)
pratheek = Professor('Pratheek',None, 19)
```
In the above snippet, **classvar** is common variable to all the instances (if you explicitly assign it or not). Therefore, even though we did not mention about classvar in pooja and pratheek, they **both will contain a class variable called classvar** which can be accessed and used whichever way you’d like.

Now, lets talk about the **classmethods**. Generally, class methods are used to modify, edit or even delete the class variables. Just like a normal method, it also takes a first argument by default but only in this case, it takes **class name as its first argument**. Also, whenever you are declaring a class method, you should enclose it using a **_@classmethod decorator_**. This is done to show Python that the method defined here is a class method and should not be mistaken with the normal method. **Class methods are usually called using the class name**. The following code snippet demonstrates the use of **classmethod**:

```python
class Professor:
    classvar = "this is a class variable" # class variable

    def __init__(self, name, id, age):
        self.name = name      # instance variable
        self.age  = age       # instance variable
        self.id   = id        # instance variable
    
    @classmethod
    def dean(cls, name):
        cls.dean = f"{name}"

#instances
pooja = Professor('Pooja', 34, 25)
pratheek = Professor('Pratheek',None, 19)

# calling the class method
Professor.dean('LPU Dean')

print(pooja.dean)       # LPU Dean
print(pratheek.dean)    # LPU Dean

```
In the above snippet, you can see that although **I haven’t declared dean attribute for pooja and pratheek, but still it got reflected later as their attributes**. This is one of the most common use case of **class methods**. They are used to set / modify class variables. You could also use instances to set the class variable, but it would be a vague or unsuitable candidate and also, I can’t really think of a case where an object can decide what class variable should the class hold.

Now, comes the **static method**. Static method, in general, does not serve other purposes rather than to use **them as logical connectors** which means that it is used to implement **some sort of logic within the class and does not affect the instances in any way.** 

Static methods **do not** take any default arguments. You can declare a static method using the **@staticmethod decorator**. It can be called using the **class name or instance name**. The following snippet shows its usage:

```python
class Professor:
    classvar = "this is a class variable" 

    def __init__(self, name, id, age):
        self.name = name      
        self.age  = age       
        self.id   = id        
    
    @classmethod
    def dean(cls, name):
        cls.dean = f"{name}"

    @staticmethod
    def displayPrompt():
        print("The details have been entered")

#instances
pooja = Professor('Pooja', 34, 25)
pooja.displayPrompt()       # calling via instance

pratheek = Professor('Pratheek',None, 19)
Professor.displayPrompt()   # calling via class name

# both prints out the same result.
```
In the above code, both `displayPrompt()` will output the same result. Also, notice that in it, we haven’t passed any default arguments.

## The End is _Not_ Nigh
We discussed a lot in this article. You are now well equipped with the fundamentals to take on this OOPs fight. In future, I will include some Inheritance articles and even some extras that many don't know about OOPs.

## Would You Like to Support Me?
If you want to support me and my contents, then go ahead and consider doing it. I would highly appreciate that:

- [YouTube Channel](https://www.youtube.com/channel/UC1QZPervOHLiC4xpVnzbDFg): Home to all sorts of **peculiar** tutos.
- [GitHub](https://www.github.com/P0intMaN): Looking forward to your PR 😉