Skip to main content

self

Introduction​

The self parameter in Python is a cornerstone of object-oriented programming (OOP) within the language. It is essential for creating and managing objects (instances) in a way that encapsulates data and behavior within classes. This guide provides a comprehensive and detailed exploration of what self is, how it functions, and why it is critical to the design of Python classes. We will also address common misconceptions and explain advanced topics like method resolution, class methods, and static methods.

The Concept of Object-Oriented Programming in Python​

Before diving into self, it's important to understand the broader context of object-oriented programming in Python. OOP is a programming paradigm centered around the concept of "objects," which are instances of classes. A class can be thought of as a blueprint that defines a set of properties (attributes) and behaviors (methods) that the objects created from the class will have.

The Role of self in Python​

In Python, when you define methods in a class, the first parameter of each method (including the __init__ constructor) is typically self. This parameter is not a keyword but a naming convention used to refer to the instance of the class that is currently being manipulated. It allows each method to access the attributes and other methods of the object it belongs to.

Consider the following class definition:

class Car:
def __init__(self, brand: str, fuel_type: str) -> None:
self.brand = brand
self.fuel_type = fuel_type

Here:

  • self in the __init__ method represents the instance of the class that is being created.
  • self.brand and self.fuel_type are attributes that will store the brand and fuel type of the car.

Instantiating Objects and the Role of self​

When you create an instance of a class, the __init__ method is automatically invoked, and self is implicitly passed by Python to refer to the new instance being created:

volvo = Car("Volvo", "Diesel")

In this case:

  • volvo is an instance of the Car class.
  • Inside the __init__ method, self.brand is set to "Volvo", and self.fuel_type is set to "Diesel" for this specific instance.

Each time a new instance is created, self allows the instance to have its own unique data.

Detailed Exploration of self​

How self Enables Instance-Specific Data​

One of the primary purposes of self is to enable each instance of a class to maintain its own data. This is crucial because, in OOP, we often need multiple objects of the same class, each with different states.

For example:

bmw = Car("BMW", "Electric")

Now, bmw is another instance of the Car class, with self.brand set to "BMW" and self.fuel_type set to "Electric". The volvo and bmw instances are independent of each other, each maintaining its own brand and fuel type.

self in Instance Methods​

When defining instance methods (i.e., methods that operate on an instance of a class), self is used to access and modify the instance's attributes and invoke other methods. For example:

class Car:
def __init__(self, brand: str, fuel_type: str) -> None:
self.brand = brand
self.fuel_type = fuel_type

def drive(self, distance: float) -> None:
print(f"Driving {self.brand} for {distance} km on {self.fuel_type}")

Here:

  • self.brand and self.fuel_type allow the drive method to access the instance-specific attributes.

  • When you call volvo.drive(10), Python automatically passes the volvo instance as the self parameter to the drive method, resulting in the output:

    Driving Volvo for 10 km on Diesel

This demonstrates how methods can operate on data specific to the instance from which they are called.

The Naming Convention of self​

It's crucial to understand that self is not a reserved keyword in Python; it is simply a convention that signals to other developers (and to yourself) that this parameter refers to the instance of the class. You can name this parameter anything, though using self is highly recommended for readability and consistency.

Consider this unconventional example:

class Car:
def __init__(this, brand: str, fuel_type: str) -> None:
this.brand = brand
this.fuel_type = fuel_type

def drive(this, distance: float) -> None:
print(f"Driving {this.brand} for {distance} km on {this.fuel_type}")

The code will work identically, but naming the parameter this instead of self may confuse other developers who are accustomed to the convention.

Self is Implicitly Passed​

A key aspect of self is that Python implicitly passes it when you call an instance method. For instance:

volvo.drive(10)

Python interprets this as:

Car.drive(volvo, 10)

Thus, self is automatically set to the instance volvo. Attempting to pass self explicitly like this would raise an error because Python expects self to be handled automatically.

Self in Constructors and Other Special Methods​

In Python, special methods like __init__, __str__, __repr__, and __eq__ also rely on self to operate on the instance. These methods are typically used to define how an object should be initialized, represented as a string, compared, and so on.

For example, consider the __str__ method, which defines how an object should be printed:

class Car:
def __init__(self, brand: str, fuel_type: str) -> None:
self.brand = brand
self.fuel_type = fuel_type

def __str__(self) -> str:
return f"{self.brand} running on {self.fuel_type}"

volvo = Car("Volvo", "Diesel")
print(volvo)

Output:

Volvo running on Diesel

Here, self in the __str__ method ensures that the correct instance-specific data is used when converting the object to a string.

Class Methods vs. Instance Methods​

While instance methods operate on instances of the class and require self, Python also supports class methods, which operate on the class itself and not on instances. Class methods are defined using the @classmethod decorator and take cls (another convention, standing for "class") as their first parameter instead of self.

Example:

class Car:
total_cars = 0

def __init__(self, brand: str, fuel_type: str) -> None:
self.brand = brand
self.fuel_type = fuel_type
Car.total_cars += 1

@classmethod
def get_total_cars(cls) -> int:
return cls.total_cars

Here:

  • get_total_cars is a class method, meaning it operates on the class level, using cls to refer to the class itself.
  • total_cars is a class attribute shared by all instances of the Car class.

Class methods are useful for factory methods, counting instances, or accessing/modifying class-level data.

Static Methods: When self Is Not Needed​

Static methods are another category of methods that do not operate on either an instance (self) or the class (cls). They are defined using the @staticmethod decorator and do not take self or cls as a parameter.

Example:

class Car:
def __init__(self, brand: str, fuel_type: str) -> None:
self.brand = brand
self.fuel_type = fuel_type

@staticmethod
def display_fuel_types() -> None:
print("Available fuel types: Diesel, Electric, Gasoline")

Car.display_fuel_types()

Here:

  • display_fuel_types is a static method and can be called without creating an instance of Car.
  • Static methods are typically utility functions that perform a task in isolation without depending on class or instance-specific data.

Self in Inheritance and Method Resolution Order (MRO)​

In Python, self is also crucial when dealing with inheritance. When a class inherits from another, the self parameter ensures that methods from the base class can be properly invoked on the subclass instances.

Consider the following example:

class Vehicle:
def __init__(self, brand: str) -> None:
self.brand = brand

def start(self) -> None:
print(f"{self.brand} vehicle is starting.")

class Car(Vehicle):
def __init__(self, brand: str, fuel_type: str) -> None:
super().__init__(brand)
self.fuel_type = fuel_type

def start(self) -> None:
print(f"{self.brand} car running on {self.fuel_type} is starting.")

Here:

  • The Car class inherits from Vehicle.
  • The super().__init__(brand) call in Car.__init__ invokes the Vehicle.__init__ method to initialize the brand attribute, with

self ensuring that the correct instance is modified.

When calling start on a Car instance:

tesla = Car("Tesla", "Electric")
tesla.start()

Output:

Tesla car running on Electric is starting.

The method resolution order (MRO) ensures that the start method from the Car class is used, but self guarantees that if the method was not overridden, the Vehicle version would still work correctly with the Car instance.

Summary​

Key Points to Remember​

  • self is a reference to the current instance of the class: It allows methods to access and modify the instance’s attributes and other methods.
  • self is automatically passed to instance methods: Python handles this implicitly, ensuring that the correct instance data is used without the need for explicit passing.
  • Naming conventions: While self is not a keyword, it is the widely accepted convention and should be used to maintain code clarity.
  • Different types of methods: Instance methods require self, class methods require cls, and static methods do not require either.
  • Inheritance and MRO: self ensures that inherited methods operate correctly on subclass instances.

Conclusion​

The self parameter in Python is integral to the correct functioning of object-oriented programming within the language. It provides the mechanism by which instances of classes maintain their own state and behavior, enabling the powerful and flexible design patterns that OOP allows. By fully understanding self, you gain a deeper insight into Python’s class mechanics, empowering you to write more effective and maintainable code.