Constructors in Python
What is a Constructor?
A constructor is a special method that runs AUTOMATICALLY when we create an object of a class. In Python, the constructor is __init__().
emp = Employee("Ravi", 50000)
|
+--> Python creates an empty object
+--> then calls Employee.__init__(emp, "Ravi", 50000)
Why Do We Need It?
To give every new object its starting data (its "initial state"). Example: every new bank account must start with an account number, an owner name and an opening balance.
Key Points to Remember
__init__is called automatically - we never call it ourselves.- The first parameter is always "
self" (the object being created). __init__must return None (it only SETS data, it does not return).- Python does NOT support multiple constructors (overloading) like Java/C++. We use default arguments or
@classmethodinstead.
Default Constructor
If we do NOT write __init__, Python gives the class a hidden default constructor that takes no arguments and does nothing. Industry example: a simple "marker" or settings holder.
class AppSettings:
theme = "light" # class variable - shared by all objects
language = "English"
settings = AppSettings() # works even though no __init__ was written
print("Theme :", settings.theme)
print("Language:", settings.language)
Non-Parameterized Constructor
__init__ only takes "self". Every object starts with the SAME values. Industry example: a new shopping cart is always empty at the start.
class ShoppingCart:
def __init__(self):
print(" -> __init__ called: creating an empty cart")
self.items = [] # instance variable (each cart has its own list)
self.total = 0.0
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.items.append("Laptop")
print("cart1 items:", cart1.items)
print("cart2 items:", cart2.items, " <- separate list, not affected")
Parameterized Constructor
__init__ takes extra arguments so each object can start with DIFFERENT data. Industry example: Employee record in an HR system.
class Employee:
def __init__(self, emp_id, name, department, salary):
# self.<name> = <value> creates an INSTANCE variable
self.emp_id = emp_id
self.name = name
self.department = department
self.salary = salary
def show(self):
print(f" [{self.emp_id}] {self.name:<10} | {self.department:<8} | Rs.{self.salary}")
e1 = Employee(101, "Anita", "IT", 75000)
e2 = Employee(102, "Rahul", "HR", 55000)
e1.show()
e2.show()
Constructor with Default Arguments
Some arguments are optional. If the caller does not give them, the default value is used. Industry example: placing a food-delivery order. Quantity defaults to 1 and delivery type defaults to "standard".
class FoodOrder:
def __init__(self, item, quantity=1, delivery="standard"):
self.item1 = item
self.quantity1 = quantity
self.delivery1 = delivery
def summary(self):
print(f" {self.quantity1} x {self.item1} ({self.delivery1} delivery)")
FoodOrder("Pizza").summary() # uses both defaults
FoodOrder("Burger", 3).summary() # overrides quantity
FoodOrder("Biryani", 2, "express").summary() # overrides both
FoodOrder("Pasta", delivery="express").summary() # keyword argument
FoodOrder("Pasta", "express").summary() # keyword argument
Instance Variables vs Class Variables
Instance variable -> belongs to ONE object (self.name)
Class variable -> shared by ALL objects (ClassName.counter)
Industry example: auto-generating unique customer IDs. The constructor increases a shared counter every time a new customer is created.
class Customer:
next_id = 1000 # CLASS variable - one copy for the whole class
bank_name = "MMC Bank" # CLASS variable - same for every customer
def __init__(self, name):
Customer.next_id += 1 # update the shared counter
self.customer_id = Customer.next_id # INSTANCE variable
self.name = name # INSTANCE variable
c1 = Customer("Meena")
c2 = Customer("Arjun")
c3 = Customer("Sara")
for c in (c1, c2, c3):
print(f" {c.customer_id} - {c.name} - {c.bank_name}")
print(" Total customers created:", Customer.next_id - 1000)
Validation Inside the Constructor
The constructor is the best place to STOP bad objects from being created. If data is invalid, raise an exception. Industry example: a bank account must have a minimum opening balance.
class BankAccount:
MIN_OPENING_BALANCE = 1000
def __init__(self, owner, opening_balance):
if not owner.strip():
raise ValueError("Owner name cannot be empty")
if opening_balance < BankAccount.MIN_OPENING_BALANCE:
raise ValueError(
f"Opening balance must be at least {BankAccount.MIN_OPENING_BALANCE}"
)
self.owner = owner
self.balance = opening_balance
print(f" Account opened for {owner} with Rs.{opening_balance}")
BankAccount("Kiran", 5000) # valid
try:
BankAccount("Divya", 200) # invalid - too little money
except ValueError as err:
print(" ERROR:", err)
Alternate Constructors Using @classmethod
Python has only ONE __init__, but we often receive data in different formats (CSV text, dictionary from JSON/API, database row...). A @classmethod can act as a "second constructor": it converts the data and then calls the normal constructor using cls(...).
Industry example: products arriving from a CSV file and from a REST API.
class Product:
def __init__(self, sku, name, price):
self.sku = sku
self.name = name
self.price = price
@classmethod
def from_csv(cls, line):
"""Create a Product from a CSV line like 'P01,Mouse,499'."""
sku, name, price = line.split(",")
return cls(sku, name, float(price)) # cls == Product
@classmethod
def from_dict(cls, data):
"""Create a Product from a dictionary (e.g. JSON from an API)."""
return cls(data["sku"], data["name"], data["price"])
def __str__(self):
return f"{self.sku}: {self.name} @ Rs.{self.price}"
p1 = Product("P00", "Keyboard", 999) # normal way
p2 = Product.from_csv("P01,Mouse,499") # from CSV
p3 = Product.from_dict({"sku": "P02", "name": "Monitor", "price": 8999}) # from API
for p in (p1, p2, p3):
print(" ", p)
"Constructor Overloading" in Python
In Java you can write many constructors with different parameters. In Python, if you write __init__ twice, the SECOND one REPLACES the first. Only the last definition survives.
class Student:
def __init__(self, name):
self.name = name
def __init__(self, name, roll_no): # this REPLACES the one above
self.name = name
self.roll_no = roll_no
try:
Student("Asha") # fails, because only the 2-argument version exists
except TypeError as err:
print(" ERROR:", err)
# The Pythonic way: use default values (or *args / **kwargs).
class StudentFixed:
def __init__(self, name, roll_no=None, course="Python"):
self.name = name
self.roll_no = roll_no
self.course = course
def __str__(self):
return f"{self.name} | roll={self.roll_no} | course={self.course}"
print(" ", StudentFixed("Asha"))
print(" ", StudentFixed("Vikram", 21))
print(" ", StudentFixed("Neha", 22, "Data Science"))
Common Mistake - Mutable Default Argument
Default values are created ONCE, when the function is defined, not every time it is called. A list default is therefore SHARED by all objects. This is a very common interview question.
class BadTeam:
def __init__(self, name, members=[]): # WRONG
self.name = name
self.members = members
t1 = BadTeam("Backend")
t2 = BadTeam("Frontend")
t1.members.append("Ajay")
print(" BadTeam t2.members:", t2.members, " <- Ajay leaked into Frontend!")
class GoodTeam:
def __init__(self, name, members=None): # CORRECT
self.name = name
self.members = members if members is not None else []
g1 = GoodTeam("Backend")
g2 = GoodTeam("Frontend")
g1.members.append("Ajay")
print(" GoodTeam g2.members:", g2.members, " <- correct, each team has its own list")
__new__ vs __init__
__new__ -> CREATES the object (allocates it) and returns it
__init__ -> INITIALIZES the object that __new__ returned
We rarely override __new__. One classic use is the SINGLETON pattern: only ONE object of the class may ever exist. Industry example: one shared database connection for the whole app.
class DatabaseConnection:
_instance = None # stores the single object
def __new__(cls, *args, **kwargs):
if cls._instance is None:
print(" -> __new__: creating the ONE connection object")
cls._instance = super().__new__(cls)
else:
print(" -> __new__: returning the existing connection")
return cls._instance
def __init__(self, host="localhost"):
self.host = host
db1 = DatabaseConnection("db.server.com")
db2 = DatabaseConnection("db.server.com")
print(" db1 is db2 ?", db1 is db2, " (same object)")
Destructor __del__
__del__ runs when the object is about to be destroyed (no variable refers to it any more). Useful for clean-up messages, but for real resources (files, sockets) prefer "with" statements. Industry example: closing a user session.
class UserSession:
def __init__(self, user):
self.user = user
print(f" Session STARTED for {user}")
def __del__(self):
print(f" Session CLOSED for {self.user}")
session = UserSession("admin")
del session # no more references -> __del__ runs now
print(" (program continues after the session is closed)")
__str__ and __repr__
__str__ -> friendly text for END USERS (used by print())
__repr__ -> exact text for DEVELOPERS/debugging (used in lists, console)
Without them, print(obj) shows something like <__main__.Invoice object at 0x...>
class Invoice:
def __init__(self, number, amount):
self.number = number
self.amount = amount
def __str__(self):
return f"Invoice #{self.number} - Amount due: Rs.{self.amount}"
def __repr__(self):
return f"Invoice(number={self.number!r}, amount={self.amount!r})"
inv = Invoice("INV-001", 2500)
print(" print(inv) ->", inv) # calls __str__
print(" repr(inv) ->", repr(inv)) # calls __repr__
print(" in a list ->", [inv]) # lists use __repr__
Quick Revision
__init__runs automatically when an object is created.- Use parameters + default values to give flexible starting data.
- Validate data in
__init__so invalid objects are never created. - Use
@classmethodfor "alternate constructors" (from_csv, from_dict). - Never use a list/dict as a default argument - use None instead.
__new__creates,__init__initializes,__del__cleans up.
Practice Questions
- Create a class Book(title, author, price=0) and print 3 books.
- Add a class variable that counts how many books were created.
- Reject a negative price in the constructor with ValueError.
- Add Book.from_string("Python Basics|Guido|450") as a classmethod.