self in Python, Demystified

If you have been programming in Python (object-oriented programming) for some time, then you have definitely come across methods that have self as their first parameter.

Let us first try to understand what this recurring self parameter is.


What is self in Python?

In object-oriented programming, whenever we define methods for a class, we use self as the first parameter in each case. Let's look at the definition of a class called Cat.

class Cat:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def info(self):
        print(f"I am a cat. My name is {self.name}. I am {self.age} years old.")

    def make_sound(self):
        print("Meow")

In this case all the methods, including __init__, have the first parameter as self.

We know that class is a blueprint for the objects. This blueprint can be used to create multiple numbers of objects. Let's create two different objects from the above class.

cat1 = Cat('Andy', 2)
cat2 = Cat('Phoebe', 3)

The self keyword is used to represent an instance (object) of the given class. In this case, the two Cat objects cat1 and cat2 have their own name and age attributes. If there was no self argument, the same class couldn't hold the information for both these objects.

However, since the class is just a blueprint, self allows access to the attributes and methods of each object in python. This allows each object to have its own attributes and methods. Thus, even long before creating these objects, we reference the objects as self while defining the class.


Why is self explicitly defined everytime?

Even when we understand the use of self, it may still seem odd, especially to programmers coming from other languages, that self is passed as a parameter explicitly every single time we define a method. As The Zen of Python goes, "Explicit is better than implicit".

So, why do we need to do this? Let's take a simple example to begin with. We have a Point class which defines a method distance to calculate the distance from the origin.

class Point(object):
    def __init__(self,x = 0,y = 0):
        self.x = x
        self.y = y

    def distance(self):
        """Find distance from origin"""
        return (self.x**2 + self.y**2) ** 0.5

Let us now instantiate this class and find the distance.

>>> p1 = Point(6,8)
>>> p1.distance()
10.0

In the above example, __init__() defines three parameters but we just passed two (6 and 8). Similarly distance() requires one but zero arguments were passed. Why is Python not complaining about this argument number mismatch?


What Happens Internally?

Point.distance and p1.distance in the above example are different and not exactly the same.

>>> type(Point.distance)
<class 'function'>
>>> type(p1.distance)
<class 'method'>

We can see that the first one is a function and the second one is a method. A peculiar thing about methods (in Python) is that the object itself is passed as the first argument to the corresponding function.

In the case of the above example, the method call p1.distance() is actually equivalent to Point.distance(p1).

Generally, when we call a method with some arguments, the corresponding class function is called by placing the method's object before the first argument. So, anything like obj.meth(args) becomes Class.meth(obj, args). The calling process is automatic while the receiving process is not (its explicit).

This is the reason the first parameter of a function in class must be the object itself. Writing this parameter as self is merely a convention. It is not a keyword and has no special meaning in Python. We could use other names (like this) but it is highly discouraged. Using names other than self is frowned upon by most developers and degrades the readability of the code (Readability counts).


Self Can Be Avoided

By now you are clear that the object (instance) itself is passed along as the first argument, automatically. This implicit behavior can be avoided while making a static method. Consider the following simple example:

class A(object):

    @staticmethod
    def stat_meth():
        print("Look no self was passed")

Here, @staticmethod is a function decorator that makes stat_meth() static. Let us instantiate this class and call the method.

>>> a = A()
>>> a.stat_meth()
Look no self was passed

From the above example, we can see that the implicit behavior of passing the object as the first argument was avoided while using a static method. All in all, static methods behave like the plain old functions (Since all the objects of a class share static methods).

>>> type(A.stat_meth)
<class 'function'>
>>> type(a.stat_meth)
<class 'function'>

Self Is Here To Stay

The explicit self is not unique to Python. This idea was borrowed from Modula-3. Following is a use case where it becomes helpful.

There is no explicit variable declaration in Python. They spring into action on the first assignment. The use of self makes it easier to distinguish between instance attributes (and methods) from local variables.

In the first example, self.x is an instance attribute whereas x is a local variable. They are not the same and they lie in different namespaces.

Many have proposed to make self a keyword in Python, like this in C++ and Java. This would eliminate the redundant use of explicit self from the formal parameter list in methods.

While this idea seems promising, it is not going to happen. At least not in the near future. The main reason is backward compatibility. Here is a blog from the creator of Python himself explaining why the explicit self has to stay.


__init__() is not a constructor

One important conclusion that can be drawn from the information so far is that the __init__() method is not a constructor. Many naive Python programmers get confused with it since __init__() gets called when we create an object.

A closer inspection will reveal that the first parameter in __init__() is the object itself (object already exists). The function __init__() is called immediately after the object is created and is used to initialize it.

Technically speaking, a constructor is a method which creates the object itself. In Python, this method is __new__(). A common signature of this method is:

__new__(cls, *args, **kwargs)

When __new__() is called, the class itself is passed as the first argument automatically(cls).

Again, like self, cls is just a naming convention. Furthermore, *args and **kwargs are used to take an arbitrary number of arguments during method calls in Python.

Some important things to remember when implementing __new__() are:

  • __new__() is always called before __init__().
  • First argument is the class itself which is passed implicitly.
  • Always return a valid object from __new__(). Not mandatory, but its main use is to create and return an object.

Let's take a look at an example:

class Point(object):

    def __new__(cls,*args,**kwargs):
        print("From new")
        print(cls)
        print(args)
        print(kwargs)

        # create our object and return it
        obj = super().__new__(cls)
        return obj

    def __init__(self,x = 0,y = 0):
        print("From init")
        self.x = x
        self.y = y

Now, let's now instantiate it.

>>> p2 = Point(3,4)
From new
<class '__main__.Point'>
(3, 4)
{}
From init

This example illustrates that __new__() is called before __init__(). We can also see that the parameter cls in __new__() is the class itself (Point). Finally, the object is created by calling the __new__() method on object base class.

In Python, object is the base class from which all other classes are derived. In the above example, we have done this using super().


Use __new__ or __init__?

You might have seen __init__() very often but the use of __new__() is rare. This is because most of the time you don't need to override it. Generally, __init__() is used to initialize a newly created object while __new__() is used to control the way an object is created.

We can also use __new__() to initialize attributes of an object, but logically it should be inside __init__().

One practical use of __new__(), however, could be to restrict the number of objects created from a class.

Suppose we wanted a class SqPoint for creating instances to represent the four vertices of a square. We can inherit from our previous class Point (the second example in this article) and use __new__() to implement this restriction. Here is an example to restrict a class to have only four instances.

class SqPoint(Point):
    MAX_Inst = 4
    Inst_created = 0

    def __new__(cls,*args,**kwargs):
        if (cls.Inst_created >= cls.MAX_Inst):
            raise ValueError("Cannot create more objects")
        cls.Inst_created += 1
        return super().__new__(cls)

A sample run:

>>> p1 = SqPoint(0,0)
>>> p2 = SqPoint(1,0)
>>> p3 = SqPoint(1,1)
>>> p4 = SqPoint(0,1)
>>> 
>>> p5 = SqPoint(2,2)
Traceback (most recent call last):
...
ValueError: Cannot create more objects
Did you find this article helpful?

Our premium learning platform, created with over a decade of experience and thousands of feedbacks.

Learn and improve your coding skills like never before.

Try Programiz PRO
  • Interactive Courses
  • Certificates
  • AI Help
  • 2000+ Challenges