Python Tutorials

Python Constructors & Attributes : init and self

Imagine buying a brand new smartphone, but when you open the box, you find a pile of disconnected parts: a screen, a battery, a camera lens, and a motherboard. To use the phone, you would have to manually assemble everything yourself. That would be a terrible user experience!

In Object-Oriented Programming (OOP), creating a new object without setting it up is exactly like handing someone a box of phone parts. To ensure an object is ready to use the exact millisecond it is created, Python provides a special setup tool called a Constructor.

By mastering constructors and attributes, you take complete control over your objects, ensuring they are born with the correct data, configurations, and identity.

Constructors & Attributes

  • Constructor (__init__): A special “magic” method in Python that is automatically called the moment a new object is created from a class. Its primary job is to initialize (set up) the object’s starting state.
  • Attributes: Variables that belong to an object. They hold the data or “state” of that specific object (e.g., color, size, username).
  • self: A mandatory keyword used inside class methods. It acts as a self-reference, allowing the object to point to itself and say, “Modify MY attributes, not the attributes of other objects.”

Syntax & Basic Usage

To define a constructor, you create a method exactly named __init__ (with two underscores on both sides). The first parameter must always be self.

# 1. Defining the class and its constructor
class Robot:
    # The constructor method automatically runs upon creation
    def __init__(self, robot_name):
        # We attach the passed argument to the object using 'self'
        self.name = robot_name
        self.battery_level = 100
        print(f"Robot '{self.name}' has been activated!")

# 2. Creating an instance (This automatically triggers __init__)
cleaning_bot = Robot("Roomba")

# 3. Accessing the object's attributes
print(f"Current Battery: {cleaning_bot.battery_level}%")

# Expected Output:
# Robot 'Roomba' has been activated!
# Current Battery: 100%

Python Constructor Methods and Function Arguments

Understanding the interplay between __init__, self, and attributes is the most critical hurdle in mastering Python OOP. Let’s break down exactly how these components work together.

1. Decoding the self Keyword

Why do we need self? If you create 100 different robots, Python needs a way to know which robot’s battery to drain. self is the magical link that binds data to a specific, individual object.

class SmartSpeaker:
    def __init__(self, device_name, volume):
        # 'self.device_name' belongs to the object. 
        # 'device_name' is just the temporary variable passed in.
        self.device_name = device_name
        self.current_volume = volume

# We create two distinct objects from the same class
kitchen_speaker = SmartSpeaker("Kitchen Echo", 40)
bedroom_speaker = SmartSpeaker("Bedroom Dot", 15)

# 'self' ensures that changing one does NOT affect the other
kitchen_speaker.current_volume = 80

print(f"{kitchen_speaker.device_name} Volume: {kitchen_speaker.current_volume}")
print(f"{bedroom_speaker.device_name} Volume: {bedroom_speaker.current_volume}")

# Expected Output:
# Kitchen Echo Volume: 80
# Bedroom Dot Volume: 15

2. Default Attribute Values in __init__

You don’t always have to force the user to provide every single piece of data when creating an object. You can provide default values directly in the constructor.

class BankAccount:
    # 'account_type' defaults to "Checking" if the user doesn't provide one
    def __init__(self, account_holder, account_type="Checking"):
        self.holder = account_holder
        self.type = account_type
        # We can also hardcode starting attributes that aren't passed in at all
        self.balance = 0.0

# Creating an account using the default type
student_account = BankAccount("Alice")

# Creating an account by overriding the default type
savings_account = BankAccount("Bob", "Savings")

print(f"{student_account.holder} opened a {student_account.type} account.")
print(f"{savings_account.holder} opened a {savings_account.type} account.")

# Expected Output:
# Alice opened a Checking account.
# Bob opened a Savings account.

3. Instance Attributes vs. Class Attributes

This is a frequent point of confusion.

  • Instance Attributes are defined inside __init__ using self. They are unique to every object.
  • Class Attributes are defined outside __init__, directly inside the class. They are globally shared by every object of that class.
class DeliveryDriver:
    # CLASS ATTRIBUTE: Shared across all drivers
    company_name = "Speedy Couriers"
    
    def __init__(self, driver_name):
        # INSTANCE ATTRIBUTE: Unique to this specific driver
        self.name = driver_name

driver_one = DeliveryDriver("Carlos")
driver_two = DeliveryDriver("Diana")

# Both drivers share the same company
print(f"{driver_one.name} works for {driver_one.company_name}")

# If the company changes its name globally...
DeliveryDriver.company_name = "Global Logistics"

# ...EVERY driver's company name is instantly updated!
print(f"{driver_two.name} now works for {driver_two.company_name}")

# Expected Output:
# Carlos works for Speedy Couriers
# Diana now works for Global Logistics

Python is incredibly flexible. You can actually attach brand new attributes to an object on the fly, long after it has been created. (Note: While possible, this is generally frowned upon as it makes your code unpredictable. It is better to define all attributes in __init__.)

class GameCharacter:
    def __init__(self, character_name):
        self.name = character_name

hero = GameCharacter("Zelda")

# Dynamically creating a brand new attribute outside of the class!
hero.equipped_weapon = "Master Sword"

print(f"{hero.name} is wielding the {hero.equipped_weapon}.")

# Expected Output:
# Zelda is wielding the Master Sword.

Real-World Practical Examples

Scenario 1: User Profile Management System

In a web application, tracking user logins and account security statuses is critical. We use __init__ to establish a safe default state when a new user registers.

class UserProfile:
    def __init__(self, username, email_address):
        self.username = username
        self.email = email_address
        self.failed_login_attempts = 0
        self.is_locked = False
        
    def record_failed_login(self):
        # If the account is already locked, do nothing
        if self.is_locked:
            print(f"Account '{self.username}' is locked. Please contact support.")
            return
            
        self.failed_login_attempts += 1
        print(f"Failed login. Attempt {self.failed_login_attempts}/3.")
        
        # Lock the account if they fail 3 times
        if self.failed_login_attempts >= 3:
            self.is_locked = True
            print(f"SECURITY ALERT: Account '{self.username}' has been locked!")

# Simulating a user registering
new_user = UserProfile("hacker123", "badguy@email.com")

# Simulating someone guessing the password incorrectly
new_user.record_failed_login()
new_user.record_failed_login()
new_user.record_failed_login()
new_user.record_failed_login() # This should hit the locked block

# Expected Output:
# Failed login. Attempt 1/3.
# Failed login. Attempt 2/3.
# Failed login. Attempt 3/3.
# SECURITY ALERT: Account 'hacker123' has been locked!
# Account 'hacker123' is locked. Please contact support.

Scenario 2: E-Commerce Product Inventory

Constructors allow us to calculate starting values based on the data provided during object creation, ensuring our objects are mathematically sound from the start.

class RetailProduct:
    def __init__(self, product_name, base_price, stock_quantity):
        self.name = product_name
        self.price = base_price
        self.stock = stock_quantity
        # Automatically calculate the total inventory value upon creation
        self.total_inventory_value = self.price * self.stock
        
    def apply_discount(self, discount_percentage):
        discount_multiplier = (100 - discount_percentage) / 100
        self.price = round(self.price * discount_multiplier, 2)
        # Recalculate inventory value after the price drops
        self.total_inventory_value = self.price * self.stock
        print(f"{self.name} discounted by {discount_percentage}%. New price: ${self.price}")

# Create a product
laptop_inventory = RetailProduct("Gaming Laptop", 1200.00, 10)

print(f"Initial Inventory Value: ${laptop_inventory.total_inventory_value}")

# Run a Black Friday sale
laptop_inventory.apply_discount(20)

print(f"New Inventory Value: ${laptop_inventory.total_inventory_value}")

# Expected Output:
# Initial Inventory Value: $12000.0
# Gaming Laptop discounted by 20%. New price: $960.0
# New Inventory Value: $9600.0

Best Practices & Common Pitfalls

  • Forgetting self in __init__: This is the most common beginner error. If you write def __init__(name):, Python will crash with a TypeError when you try to create an object. self must always be the first parameter.
  • Returning Values from __init__: The constructor’s only job is to set things up. It is strictly forbidden from returning a value. If you write return "Success" inside __init__, Python will throw a fatal TypeError. __init__ must always return None (which happens automatically if you just omit the return keyword).
  • Define All Attributes in __init__: While Python lets you define new attributes inside other methods (e.g., self.new_var = 5 inside a def play_game(self): method), this is considered bad practice. Another developer looking at your class won’t know new_var exists until that specific method is run. Always define all instance attributes inside __init__, even if you just set them to None or 0 initially.

Summary

  • The Constructor is defined using the __init__ method and runs automatically the moment a new object is instantiated.
  • It is used to initialize an object’s starting state, ensuring it is ready for immediate use.
  • self is a mandatory reference that allows the object to attach data to itself, keeping it entirely separate from other objects of the same class.
  • Instance Attributes (self.variable) hold data unique to one specific object.
  • Class Attributes (defined outside __init__) hold data shared globally by all objects built from that class.
  • You can assign default values to attributes directly in the constructor parameters, making object creation highly flexible.

Leave a Comment