Skip to content

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 @classmethod instead.

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 @classmethod for "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

  1. Create a class Book(title, author, price=0) and print 3 books.
  2. Add a class variable that counts how many books were created.
  3. Reject a negative price in the constructor with ValueError.
  4. Add Book.from_string("Python Basics|Guido|450") as a classmethod.