Skip to content

The Role of `self` in Object-Oriented Python

Updated: at 02:12 AM

Object-oriented programming (OOP) is a fundamental programming paradigm in Python that structures code into reusable, modular objects. A key component of OOP is the self keyword, which plays an important role in accessing object attributes and methods. This guide will provide an in-depth explanation of self in Python OOP, including how it enables encapsulation and access to class attributes. We will cover key concepts and usage of self with examples to help Python developers utilize it effectively.

Table of Contents

Open Table of Contents

Introduction to self

In Python, self refers to the instance of a class. It is a reference to the object itself, which gives access to the object’s attributes and methods from within the class definition. Here is a simple example:

class Car:

    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def drive(self):
        print(f"Driving the {self.make} {self.model}")

my_car = Car('Toyota', 'Prius', 2020)
my_car.drive()
# Prints "Driving the Toyota Prius"

When my_car is instantiated as an object of the Car class, self refers to that my_car instance itself. Inside the drive() method, we can access the make and model attributes of the my_car object using self.make and self.model.

The self parameter is required in method definitions and __init__() constructors and automatically passed when a method is called on an instance. This allows each instance to have its own self referencing its particular object state.

Why Use self?

self enables encapsulation and data hiding in OOP. It binds the attributes and methods to the class instance objects themselves. Without using self, the properties and behaviors would be unbound or global which breaks encapsulation.

Additionally, self provides a way to access, operate on, or change the state of an object from within the class definition. This is essential for modifying object attributes in methods.

Let’s compare examples with and without self:

Without Self

class Car:

    make = 'Ford'

    def drive():
        print(f"Driving the {make}")

my_car = Car()
my_car.drive()
# Error! 'make' is not defined

With Self

class Car:

    def __init__(self, make):
        self.make = make

    def drive(self):
        print(f"Driving the {self.make}")

my_car = Car('Ford')
my_car.drive() # Prints "Driving the Ford"

By using self.make instead of just make, the drive() method can access the make attribute of that specific instance.

How self Works in Python

When a method is called on an object, Python will pass the object instance as the first argument automatically. This gets assigned to self in the method definition.

For example:

class Person:

    def __init__(self, name):
        self.name = name

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

john = Person('John')
john.print_name()

# Behind the scenes:
# 1. john is passed as self when print_name() is called
# 2. self.name in print_name() refers to john.name

This automatic passing of the object instance to self happens both for explicitly defined methods like print_name() above and implicitly defined special methods like __init__().

In fact, self only exists within the scope of the class. Trying to use it outside the class definition will raise an error:

print(self) # Raises NameError

Using self in Object Methods

self should be used whenever you need to access attributes or call methods on the object from within the class definition.

For example, we can modify attributes:

class Person:

    def __init__(self, name):
        self.name = name

    def change_name(self, new_name):
        self.name = new_name # modifies the name attribute

p = Person('Alice')
p.change_name('Bob')
print(p.name) # 'Bob'

Or access other methods on the same object:

class Person:

    def __init__(self, name):
        self.name = name

    def print_name(self):
        print(f"My name is {self.name}")

    def print_hello(self):
        self.print_name() # Call another method
        print("Hello!")

p = Person('Alice')
p.print_hello()
# My name is Alice
# Hello!

These patterns demonstrate how self provides access to the internal state and behaviors of an object programmatically from within the class.

self in the Constructor (__init__)

The constructor method __init__ is called automatically each time a new instance of a class is created. It initializes the attributes of the object.

Any parameters passed during object creation get passed into __init__() after self. Then we can set them as attributes of self:

class Person:

    def __init__(self, name, age):
        self.name = name # name arg passed in becomes name attribute
        self.age = age # age arg passed in becomes age attribute

p = Person('Alice', 25)

Now the name and age attributes are initialized on the Person instance p.

Modifying self Attributes

As shown earlier, we can modify attributes of self in any method:

class Person:

    def __init__(self, name):
        self.name = name

    def change_name(self, new_name):
        self.name = new_name

This allows encapsulating data change logic conveniently within class methods.

However, it is also possible to modify attributes directly:

p = Person('Alice')
p.name = 'Bob' # Directly change name

While simple for small scripts, it is better OOP practice to use accessor methods to change attributes rather than external direct manipulation.

Accessing self from Another Method

Sometimes we want to access the self instance from another helper method within the class:

class Person:

    def __init__(self, name):
        self.name = name

    def print_nametag(self):
        print(self._get_nametag())

    def _get_nametag(self):
        return f"Name: {self.name}" # Access self.name here

By convention, helper methods that only should be called internally are prefixed with an underscore _.

This allows encapsulating useful logic while preventing external access to private helpers that are only intended for internal use.

Passing self to Other Methods

When defining class methods that call each other, best practice is to pass self explicitly rather than rely on the automatic passing.

For example:

class Person:

    def __init__(self, name):
        self.name = name

    def print_name(self, obj):
        print(obj.name)

    def print_hello(self):
        self.print_name(self) # Pass self explicitly

This makes it clear that you want self to be passed from one method to another within the class.

self in Inheritance and Polymorphism

The meaning and usage of self extends into two other major OOP concepts - inheritance and polymorphism.

With inheritance, self refers to the instance of the subclass that inherits from the parent class:

class Vehicle:

    def __init__(self, make, model):
        self.make = make
        self.model = model

    def print_details(self):
        print(f"{self.make} {self.model}")


class Car(Vehicle):

    def open_trunk(self):
        print(f"Opening trunk of {self.make} {self.model}")

c = Car('Toyota', 'Prius')
c.print_details() # Toyota Prius
c.open_trunk() # Opening trunk of Toyota Prius

For polymorphism, self allows different subclass objects to be substituted for a parent class object while behaving appropriately based on the actual subclass type:

class Vehicle:

    def desc(self):
        print(f"A {self.kind}")


class Car(Vehicle):

    def __init__(self):
        self.kind = 'car'


class Truck(Vehicle):

    def __init__(self):
        self.kind = 'truck'

vehicles = [Car(), Truck()]

for vehicle in vehicles:
    vehicle.desc() # polymorphic use of desc() method
# A car
# A truck

The same desc() method, when called with different subclass versions of self, can have polymorphic results.

When Not to Use self

Sometimes instance attributes or methods don’t require access to self.

For example, simple utility functions:

import math

class Circle:

    def area(self, radius):
        return math.pi * (radius ** 2) # doesn't need self

Or constant class attributes:

class Math:

    PI = 3.14159 # constant value, no self needed

Here self is unneeded because the utilities are self-contained and stateless.

Best Practices for Using self

To properly leverage self in Python OOP, keep these best practices in mind:

Adhering to these practices will result in clean, encapsulated object-oriented code.

Common Uses of self

To summarize, here are some of the most common uses of the self parameter in Python OOP:

Understanding these core use cases will help unlock the full potential of using self in your Python objects.

Conclusion

The self parameter is a fundamental concept for object-oriented programming in Python. It enables encapsulation and access to object state and behaviors.

Within a class definition, self refers to the instance being operated on. It provides a gateway to class attributes and methods in each unique object instance. Proper use of self results in modular, reusable code that adheres to key OOP principles.

Hopefully this guide provided you with a firm understanding of how to leverage self effectively in your own Python classes. Some key takeaways:

With the power of self in your toolbox, you can write concise yet powerful object-oriented code in Python!