Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

18. Explain Python's `__init__` and `self` keywords in classes.

In Python's object-oriented programming (OOP), `__init__` and `self` are fundamental components for defining classes and creating objects (instances) from them. They work together to initialize the state of new objects.

1. `__init__` Method:

  • Purpose: The `__init__` method is a special method in Python classes, known as the **constructor** or **initializer**. It is automatically called when a new instance (object) of the class is created.
  • Role: Its primary role is to initialize the attributes (data) of the newly created object. This involves assigning initial values to instance variables or performing any setup operations required for the object to be in a valid state.
  • Syntax: It is defined like a regular function within the class, but it always starts and ends with double underscores (dunder methods), which makes it a special method in Python. It takes at least one argument, conventionally named `self`.
    
    class MyClass:
        def __init__(self, arg1, arg2, ...):
            # Initialization code here
                
  • Return Value: The `__init__` method does not explicitly return a value. It implicitly returns `None`. Its purpose is to set up the object, not to compute a result.

2. `self` Keyword:

  • Purpose: `self` is the conventional (but not enforced) name for the **first parameter of any instance method** in a Python class. When a method is called on an object (e.g., `my_object.method()`), Python automatically passes the instance `my_object` itself as the first argument to the method. This first argument is traditionally named `self`.
  • Role:
    • It serves as a reference to the **current instance of the class**.
    • It allows you to access and modify the instance's attributes (e.g., `self.attribute_name`) and call other instance methods (e.g., `self.another_method()`) from within any method of the class.
    • When you define `self.attribute = value` inside `__init__`, you are creating an instance attribute that belongs uniquely to that specific object.
  • Why `self` is explicitly required: Unlike some other object-oriented languages (like Java or C++ where `this` is implicitly available), Python explicitly requires `self` as the first parameter. This design choice makes it clear that the method is operating on the instance itself, improving readability and making it easier to distinguish instance attributes/methods from local variables.

Relationship:

The `__init__` method uses `self` to receive the newly created (but not yet initialized) instance of the class. Inside `__init__`, you then use `self` to assign initial values to that instance's attributes, effectively setting up its initial state.

For example, if you have `class Dog: def __init__(self, name): self.name = name`, when you do `my_dog = Dog("Buddy")`, Python internally does something like:

  1. Creates an empty `Dog` object.
  2. Calls `my_dog.__init__(my_dog, "Buddy")`.
  3. Inside `__init__`, `self` refers to `my_dog`, and `name` refers to `"Buddy"`.
  4. `self.name = name` effectively sets `my_dog.name = "Buddy"`.

Both `__init__` and `self` are cornerstone concepts for object-oriented programming in Python, enabling the creation and management of distinct objects with their own data and behaviors.


# --- Example 1: Basic Class with __init__ and self ---

class Car:
    """
    A class to represent a car with make, model, and year.
    """
    # The __init__ method is the constructor.
    # 'self' refers to the instance of the Car class being created.
    # 'make', 'model', 'year' are parameters for initialization.
    def __init__(self, make, model, year):
        print(f"Initializing a new Car object...")
        # Assigning parameters to instance attributes using 'self'.
        # self.make, self.model, self.year are unique to each Car object.
        self.make = make
        self.model = model
        self.year = year
        self.mileage = 0 # Default attribute

    # An instance method using 'self' to access instance attributes.
    def display_info(self):
        """Prints the car's information."""
        print(f"Car Info: {self.year} {self.make} {self.model}, Mileage: {self.mileage} miles")

    # Another instance method that modifies an instance attribute
    def drive(self, miles):
        """Simulates driving the car, increasing mileage."""
        if miles > 0:
            self.mileage += miles
            print(f"Drove {miles} miles. Total mileage: {self.mileage}")
        else:
            print("Cannot drive negative miles.")

# Creating instances (objects) of the Car class
print("--- Creating Car Objects ---")
car1 = Car("Toyota", "Camry", 2020) # __init__ is called automatically
car2 = Car("Honda", "Civic", 2022)  # __init__ is called automatically

# Accessing instance attributes
print(f"\nCar1's make: {car1.make}")
print(f"Car2's model: {car2.model}")

# Calling instance methods
print("\n--- Calling Car Methods ---")
car1.display_info() # Python implicitly passes car1 as 'self'
car2.display_info() # Python implicitly passes car2 as 'self'

car1.drive(150) # Modifies car1.mileage
car2.drive(50)  # Modifies car2.mileage

car1.display_info()
car2.display_info()

# Demonstrate that attributes are unique to each instance
print(f"\nAre car1 and car2 the same object? {car1 is car2}")
print(f"car1's mileage: {car1.mileage}, car2's mileage: {car2.mileage}")


# --- Example 2: Class with optional __init__ parameters and internal methods ---
print("\n--- Class with Optional Parameters and Internal Calls ---")

class Robot:
    def __init__(self, name, version="1.0"):
        self.name = name
        self.version = version
        self.is_on = False
        print(f"{self.name} (v{self.version}) created.")

    def _power_on(self): # Internal method (conventionally private)
        self.is_on = True
        print(f"{self.name} is powered on.")

    def _power_off(self): # Internal method
        self.is_on = False
        print(f"{self.name} is powered off.")

    def start(self):
        if not self.is_on:
            self._power_on() # Calling another instance method using self
        else:
            print(f"{self.name} is already running.")

    def stop(self):
        if self.is_on:
            self._power_off() # Calling another instance method using self
        else:
            print(f"{self.name} is already off.")

my_robot = Robot("RoboUnit")
my_robot.start()
my_robot.stop()

another_robot = Robot("AlphaBot", "2.1")
another_robot.start()
another_robot.start() # Already on
        

Explanation of the Example Code:

  • **`Car` Class Example:**
    • **`__init__(self, make, model, year)`:** This is the constructor. When `car1 = Car("Toyota", "Camry", 2020)` is called, Python automatically creates an empty `Car` object and then calls `car1.__init__(car1, "Toyota", "Camry", 2020)`.
    • **`self` in `__init__`:** Inside `__init__`, `self` refers to this newly created `car1` object. `self.make = make` assigns the value of the `make` parameter (`"Toyota"`) to an attribute named `make` that *belongs to* `car1`. Similarly for `model`, `year`, and `mileage`. These attributes (`car1.make`, `car1.model`, etc.) are unique to `car1`.
    • **`display_info(self)` and `drive(self, miles)`:** These are instance methods. When `car1.display_info()` is called, Python implicitly passes `car1` as the `self` argument to the method. Inside `display_info`, `self.make` correctly accesses `car1`'s `make` attribute. The same logic applies to `car2`.
    • `car1.drive(150)` modifies `car1.mileage`. `car2.drive(50)` modifies `car2.mileage`. The `display_info()` calls after driving clearly show that each car object maintains its own independent `mileage` attribute.
  • **`Robot` Class Example:**
    • This example further demonstrates `__init__` with a default parameter (`version="1.0"`).
    • It also shows how `self` is used within methods (`start`, `stop`) to call other instance methods (`_power_on`, `_power_off`). The explicit `self.` prefix makes it clear that these are methods of the current `Robot` instance, not standalone functions.
    • The `is_on` attribute is initialized in `__init__` and then updated by the `_power_on` and `_power_off` methods, again showing how instance-specific state is managed through `self`.

These examples illustrate that `__init__` is vital for setting up the initial state of an object, and `self` is the indispensable reference that allows methods to interact with and manage the unique data and behavior of the particular object they are called upon.