Python Classes, Objects & __init__

๐Ÿ Python 3.12+ ๐ŸŸข Chapter 30 of 65 ๐Ÿ“‚ Phase 7: Object-Oriented Programming ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: OOP Concepts ยท Class Blueprint vs Object Instance ยท __init__() Constructor ยท self Parameter ยท Instance vs Class Variables
Master foundational Object-Oriented Programming in Python: the paradigm shift from procedural to object-oriented code, the class blueprint analogy, the __init__() constructor, the self memory pointer, and the crucial distinction between instance variables and class variables.
1What is OOP? Procedural vs Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "Objects" which contain both Data (in the form of fields or attributes) and Code (in the form of procedures or methods).

Procedural vs Object-Oriented Programming:

FeatureProcedural ProgrammingObject-Oriented Programming (OOP)
Core FocusFunctions and step-by-step algorithmsReal-world entities (Objects) and state
Data HandlingData is separated from functions (global state risks)Data and methods are encapsulated together
ReusabilityFunction calls onlyInheritance, Polymorphism & Composition
SecurityData is freely accessible across modulesAccess control (Encapsulation / Private fields)

The Blueprint Analogy: A Class is like an architectural blueprint for a house. The blueprint itself is not a physical house; it defines the dimensions, rooms, and doors. An Object (or Instance) is the actual physical house built from that blueprint in memory!

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Class Blueprint: BankAccount โ”‚ โ”‚ โ”œโ”€โ”€ Attributes: owner, balance, account_number โ”‚ โ”‚ โ””โ”€โ”€ Methods: deposit(), withdraw(), show_balance() โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ [Instantiate: Account("Ravi", 5000)] โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Heap Memory Object Instance: (id: 0x7fa28b) โ”‚ โ”‚ โ”œโ”€โ”€ owner = "Ravi" โ”‚ โ”‚ โ””โ”€โ”€ balance = 5000 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
๐Ÿ’ป Example 1: Defining a Class and Instantiating Objects
# Defining your first Class and Instantiating an Object:
class Car:
    """A blueprint representing a motor vehicle."""
    
    # The __init__ method initializes attributes for every new instance:
    def __init__(self, brand, model, year):
        self.brand = brand  # Instance variable
        self.model = model  # Instance variable
        self.year = year    # Instance variable
        self.is_running = False

    # Instance method:
    def start_engine(self):
        self.is_running = True
        return f"๐Ÿš— {self.brand} {self.model}'s engine is now RUNNING! Vroom!"

# Instantiating two independent objects from the Car blueprint:
car1 = Car("Tesla", "Model 3", 2024)
car2 = Car("Toyota", "Supra", 2023)

print("Car 1 Brand:", car1.brand, "| Model:", car1.model)
print("Car 2 Brand:", car2.brand, "| Model:", car2.model)
print(car1.start_engine())
๐Ÿ” Step-by-Step Breakdown:
  • class Car: defines the class template.
  • car1 = Car("Tesla", "Model 3", 2024) allocates memory on the heap and executes __init__.
  • car1 and car2 maintain completely independent attributes in separate memory slots.
2The __init__() Constructor & The self Parameter Demystified

In Python, __init__() is a special dunder (double-underscore) method known as the Instance Initializer / Constructor. It is automatically called by CPython whenever a new object is instantiated.

What is self?

self represents the exact instance of the object currently calling the method. When you write account.deposit(500), Python translates this under the hood to BankAccount.deposit(account, 500)!

Caller Code: account1.show_balance() CPython Translation: BankAccount.show_balance(account1) <-- 'account1' is passed as 'self'!

Because Python passes the active object as the first argument automatically, every instance method must explicitly declare self as its first parameter.

๐Ÿ’ป Example 2: BankAccount Class with __init__ and self
# BankAccount Class Demonstration:
class BankAccount:
    def __init__(self, owner, balance):
        # self binds the arguments to this specific instance:
        self.owner = owner
        self.balance = float(balance)

    def deposit(self, amount):
        if amount <= 0:
            print("โŒ Deposit amount must be positive!")
            return
        self.balance += amount
        print(f"โœ… Deposited โ‚น{amount:,.2f} to {self.owner}'s account.")

    def show_balance(self):
        print(f"๐Ÿ’ณ Account Holder: {self.owner} | Current Balance: โ‚น{self.balance:,.2f}")

# Creating account and invoking methods:
account = BankAccount("Ravi", 5000)
account.show_balance()
account.deposit(1000)
account.show_balance()
๐Ÿ” Why self is mandatory:

Without self.balance, Python would create a temporary local variable inside deposit() that would be discarded the moment the function finishes. self.balance attaches the variable to the object permanently.

3Instance Variables vs Class Variables

One of the most critical concepts in OOP is the distinction between variable scopes:

  • Instance Variables (self.name): Variables defined inside __init__ using self. Every object instance has its own unique, private copy stored in its __dict__ table.
  • Class Variables: Variables declared directly inside the class body outside any method. A single shared copy exists in memory for the entire class, shared across all instances!
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Class: Employee (Class Variable: company = "TechCorp")โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”œโ”€โ”€ Instance 1 (emp1): name="Balaji", salary=95000 โ”‚ โ”‚ โ””โ”€โ”€ Instance 2 (emp2): name="Alex", salary=75000 โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
๐Ÿ’ป Example 3: Instance Variables vs Shared Class Variables
class Employee:
    # 1. Class Variable (Shared across ALL employees):
    company_name = "Tech Innovations Ltd"
    total_employees_count = 0

    def __init__(self, name, department, salary):
        # 2. Instance Variables (Unique to EACH employee):
        self.name = name
        self.department = department
        self.salary = salary
        
        # Increment shared class counter:
        Employee.total_employees_count += 1

    def get_details(self):
        return f"โ€ข {self.name:8} ({self.department}) | Salary: โ‚น{self.salary:,} | Company: {self.company_name}"

# Creating employee instances:
emp1 = Employee("Balaji", "Backend", 95000)
emp2 = Employee("Alex", "DevOps", 82000)
emp3 = Employee("Chloe", "AI/ML", 105000)

print(emp1.get_details())
print(emp2.get_details())
print(emp3.get_details())
print(f"\n๐Ÿข Total Registered Employees across company: {Employee.total_employees_count}")
๐Ÿ” Namespace Resolution:

When accessing emp1.company_name, Python first checks emp1.__dict__. If not found, it falls back to Employee.__dict__.

โš ๏ธ Common Developer Pitfall: Accidentally Modifying a Class Variable via an Instance (Shadowing)

Writing emp1.company_name = "NewCorp" does NOT change the class variable for all employees. It creates a new instance variable on emp1 that shadows the class variable! Always modify class variables via the Class name: Employee.company_name = "NewCorp".

๐Ÿ’ป Hands-on Interactive Practice Challenge

Create a Book class with title, author, and price attributes. Add a method apply_discount(percent) that reduces the price in place.

Python 3 Practice Challenge โ–ถ Run in Compiler
class Book:
    def __init__(self, title, author, price):
        self.title = title
        self.author = author
        self.price = price

    def apply_discount(self, percent):
        discount_amt = self.price * (percent / 100)
        self.price -= discount_amt
        print(f"Discount {percent}% applied on '{self.title}'! New Price: โ‚น{self.price:.2f}")

book1 = Book("Python Mastery 2026", "Guido", 799.00)
book1.apply_discount(15)
Run This Challenge in Online Python IDE โ†’
โ“ Frequently Asked Questions (FAQ)

Q Why do Python methods require "self" as the first argument explicitly?

Explicit self adheres to Python's Zen principle: "Explicit is better than implicit". It makes instance variable access unambiguous and eliminates hidden scoping magic.

Q Can a class have multiple __init__ constructors in Python?

No. Python does not support traditional method overloading by signature. Defining multiple __init__ methods simply overrides previous ones. Instead, use default arguments (*args, **kwargs) or @classmethod alternative constructors.

Q What is the __del__ method in Python?

__del__ is the destructor method called when an object's reference count reaches zero right before CPython garbage collection frees its memory.

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on Python 3.12+ runtime ยท Last updated August 2026