Python Classes, Objects & __init__
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:
| Feature | Procedural Programming | Object-Oriented Programming (OOP) |
|---|---|---|
| Core Focus | Functions and step-by-step algorithms | Real-world entities (Objects) and state |
| Data Handling | Data is separated from functions (global state risks) | Data and methods are encapsulated together |
| Reusability | Function calls only | Inheritance, Polymorphism & Composition |
| Security | Data is freely accessible across modules | Access 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!
# 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())
class Car:defines the class template.car1 = Car("Tesla", "Model 3", 2024)allocates memory on the heap and executes__init__.car1andcar2maintain completely independent attributes in separate memory slots.
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)!
Because Python passes the active object as the first argument automatically, every instance method must explicitly declare self as its first parameter.
# 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()
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.
One of the most critical concepts in OOP is the distinction between variable scopes:
- Instance Variables (
self.name): Variables defined inside__init__usingself. 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:
# 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}")
When accessing emp1.company_name, Python first checks emp1.__dict__. If not found, it falls back to Employee.__dict__.
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".
Create a Book class with title, author, and price attributes. Add a method apply_discount(percent) that reduces the price in place.
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)
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.