How to create a constructor in Python

Learn how to create constructors in Python. Explore different methods, get practical tips, see real-world examples, and debug common errors.

How to create a constructor in Python
Published on: 
Tue
Mar 10, 2026
Updated on: 
Fri
Mar 13, 2026
The Replit Team

In Python, constructors initialize object states. The special __init__ method automatically runs when you create a new class instance, which sets up its initial attributes and properties.

In this article, you'll learn techniques to define constructors effectively. You will also find practical tips, see real-world applications, and get advice to debug common issues with your constructors.

Basic __init__ constructor

class Person:
   def __init__(self, name, age):
       self.name = name
       self.age = age

person = Person("Alice", 30)
print(f"Name: {person.name}, Age: {person.age}")--OUTPUT--Name: Alice, Age: 30

The __init__ method serves as the constructor for the Person class. Its primary role is to bind data to the specific instance being created. The self parameter is crucial—it’s a reference to this new object, which lets you assign the passed name and age as instance attributes.

This design ensures each Person object starts with its own distinct state. When you call Person("Alice", 30), Python automatically runs __init__, passing the new instance as the self argument along with "Alice" and 30.

Basic constructor techniques

Building on the basic __init__ method, you can enhance your constructors with default parameters, type hints, and validation to make them more robust and flexible.

Using constructors with default parameters

class Product:
   def __init__(self, name, price=0.0, stock=0):
       self.name = name
       self.price = price
       self.stock = stock

apple = Product("Apple", 1.25, 100)
default_product = Product("Unknown")
print(f"{apple.name}: ${apple.price}, Stock: {apple.stock}")
print(f"{default_product.name}: ${default_product.price}, Stock: {default_product.stock}")--OUTPUT--Apple: $1.25, Stock: 100
Unknown: $0.0, Stock: 0

Setting default values for parameters like price=0.0 and stock=0 in your __init__ method adds flexibility. It means you don't have to supply every argument when creating an object. While name is required, other attributes can be optional.

  • When you create an instance like Product("Apple", 1.25, 100), you're providing specific values that override the defaults.
  • If you only provide the required argument, as in Product("Unknown"), the constructor automatically assigns the default values to price and stock.

Constructors with type hints

from typing import Optional, List

class Student:
   def __init__(self, name: str, age: int, grades: Optional[List[float]] = None):
       self.name = name
       self.age = age
       self.grades = grades if grades else []

student = Student("Bob", 21, [90.5, 85.0, 92.3])
print(f"Student: {student.name}, Age: {student.age}, Grades: {student.grades}")--OUTPUT--Student: Bob, Age: 21, Grades: [90.5, 85.0, 92.3]

Type hints add a layer of clarity to your constructors. They're like labels that declare the expected data type for each parameter, such as name: str. While Python doesn't enforce these types at runtime, they make your code self-documenting and easier for tools to analyze.

  • The grades parameter is typed as Optional[List[float]], which signals that it can be either a list of floats or None.
  • The expression self.grades = grades if grades else [] safely handles the default value. It creates a new empty list for each student who doesn't have grades, preventing different objects from sharing the same list by mistake.

Constructors with validation

class BankAccount:
   def __init__(self, account_number, balance):
       if not isinstance(account_number, str) or len(account_number) != 10:
           raise ValueError("Account number must be a 10-character string")
       if balance < 0:
           raise ValueError("Balance cannot be negative")
       self.account_number = account_number
       self.balance = balance

account = BankAccount("1234567890", 1000)
print(f"Account: {account.account_number}, Balance: ${account.balance}")--OUTPUT--Account: 1234567890, Balance: $1000

Validation within the __init__ method ensures your objects are created with valid data from the start. It’s a gatekeeper, checking inputs before they're assigned to instance attributes.

  • The constructor for BankAccount verifies that the account_number is a 10-character string and the balance isn't negative.
  • If the data fails these checks, the constructor raises a ValueError, which stops the object from being created with incorrect information. This practice helps maintain data integrity throughout your application.

Advanced constructor patterns

With a solid foundation in basic constructors, you can now explore advanced patterns like alternative constructors, flexible arguments with *args and **kwargs, and automatic generation.

Alternative constructors with class methods

class Date:
   def __init__(self, year, month, day):
       self.year = year
       self.month = month
       self.day = day
   
   @classmethod
   def from_string(cls, date_string):
       year, month, day = map(int, date_string.split('-'))
       return cls(year, month, day)
   
date1 = Date(2023, 10, 15)
date2 = Date.from_string("2023-10-15")
print(f"Date 1: {date1.year}-{date1.month}-{date1.day}")
print(f"Date 2: {date2.year}-{date2.month}-{date2.day}")--OUTPUT--Date 1: 2023-10-15
Date 2: 2023-10-15

Alternative constructors provide flexible ways to create objects beyond the standard __init__. In the Date class, the @classmethod decorator defines from_string as a secondary way to build an instance.

  • The main __init__ constructor requires separate integers for the year, month, and day.
  • The from_string method accepts a single date string, parses it, and then calls the primary constructor using cls(year, month, day).

Here, cls is a reference to the Date class itself. This pattern is useful for creating objects from different data sources.

Constructors with *args and **kwargs

class ConfigManager:
   def __init__(self, config_file, *args, **kwargs):
       self.config_file = config_file
       self.args = args
       self.kwargs = kwargs
       
config = ConfigManager("settings.ini", "arg1", "arg2", debug=True, env="production")
print(f"Config file: {config.config_file}")
print(f"Args: {config.args}")
print(f"Kwargs: {config.kwargs}")--OUTPUT--Config file: settings.ini
Args: ('arg1', 'arg2')
Kwargs: {'debug': True, 'env': 'production'}

Using *args and **kwargs in your __init__ method creates a flexible constructor that can handle a variable number of inputs. After accounting for required parameters like config_file, the constructor neatly organizes any remaining arguments.

  • *args captures all extra positional arguments (like "arg1" and "arg2") into a tuple.
  • **kwargs collects all extra keyword arguments (like debug=True) into a dictionary.

This approach is ideal when you need to pass an open-ended set of configuration options to an object upon its creation.

Using dataclasses for automatic constructors

from dataclasses import dataclass, field
from typing import List

@dataclass
class Employee:
   name: str
   id: int
   department: str
   skills: List[str] = field(default_factory=list)

employee = Employee("John Doe", 12345, "Engineering", ["Python", "SQL"])
print(employee)--OUTPUT--Employee(name='John Doe', id=12345, department='Engineering', skills=['Python', 'SQL'])

The @dataclass decorator is a powerful shortcut for creating classes that primarily store data. It automatically generates a complete __init__ method for you based on the type-hinted attributes you define, which means you don't have to write that boilerplate code yourself.

  • This simplifies your class definition, making it cleaner and less prone to errors.
  • For mutable default values, like a list for skills, you use field(default_factory=list). This is a crucial safety feature that ensures each new object gets its own unique list, preventing them from accidentally sharing the same one.

Move faster with Replit

Replit is an AI-powered development platform that transforms natural language into working applications. You can describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.

The constructor patterns you've learned, from basic __init__ methods to advanced dataclasses, are the building blocks for real software. Replit Agent can take these concepts and turn them into production-ready tools:

  • Build a user registration system where the constructor validates new sign-ups for correct email formats and password strength.
  • Create a data import tool that uses alternative constructors to create objects from different sources, like a JSON file or a database row.
  • Deploy an inventory management app that uses dataclasses to automatically generate constructors for products with default stock levels.

Describe your app idea, and Replit Agent will write the code, test it, and fix issues automatically, all within your browser. Try Replit Agent to bring your next project to life.

Common errors and challenges

Even experienced developers run into a few common pitfalls when working with Python constructors, but they’re easy to avoid once you know what to look for.

Forgetting self in constructor parameters

A frequent mistake is forgetting to include self as the first parameter in the __init__ method. Because Python automatically passes the instance as the first argument, omitting self causes a mismatch between the arguments provided and the parameters defined.

This oversight immediately triggers a TypeError, as Python tries to assign the first argument you provided (e.g., a name) to the parameter that should have been self. Always ensure __init__ starts with def __init__(self, ...).

Mutable default arguments in constructors

Using mutable objects like a list or dictionary as default arguments in a constructor is a classic trap. Python evaluates default arguments only once when the function is defined, not every time it’s called. This means all instances created without that specific argument will share the exact same list or dictionary.

If one object modifies the shared default, the change unexpectedly appears in all other objects. The correct pattern is to set the default to None and then create a new mutable object inside the __init__ method if the argument is None.

Inheritance and forgetting to call parent __init__

When you create a subclass that has its own __init__ method, you must explicitly call the parent class’s constructor. If you don't, the parent's initialization logic never runs, and your object may be missing essential attributes.

Forgetting this step often leads to AttributeError exceptions later when your code tries to access an attribute that was supposed to be set by the parent class. The standard way to ensure proper initialization is to call super().__init__() at the beginning of the child class’s constructor.

Forgetting self in constructor parameters

Forgetting self in an __init__ method is a common slip-up that results in a confusing TypeError. Python expects the instance as the first argument, but without self to catch it, the parameters don't line up. The following code demonstrates this issue.

class Rectangle:
   def __init__(width, height):
       width = width
       height = height
   
   def area(self):
       return self.width * self.height

rect = Rectangle(10, 5)
print(f"Area: {rect.area()}")

Since the __init__ method is missing self, the width and height values aren't attached to the instance. The assignments are only local, so the area() method fails. The corrected code below shows how to fix this.

class Rectangle:
   def __init__(self, width, height):
       self.width = width
       self.height = height
   
   def area(self):
       return self.width * self.height

rect = Rectangle(10, 5)
print(f"Area: {rect.area()}")

The corrected code works because it includes self as the first parameter in the __init__ method. This allows you to attach the width and height values directly to the object instance using self.width and self.height. Without self, the assignments are only local variables that disappear after the method runs. This simple fix ensures the attributes are stored with the object, making them available to other methods like area().

Mutable default arguments in constructors

It’s a classic mistake to use a mutable default argument like items=[] in a constructor. Because Python creates the default list just once, every instance that relies on it ends up sharing the exact same list, leading to unexpected behavior.

The code below shows what happens when two different TodoList objects unintentionally modify the same list.

class TodoList:
   def __init__(self, name, items=[]):
       self.name = name
       self.items = items
   
   def add_item(self, item):
       self.items.append(item)

list1 = TodoList("Work")
list1.add_item("Finish project")
list2 = TodoList("Personal")
list2.add_item("Buy groceries")
print(list1.items)
print(list2.items)

Since both list1 and list2 fall back on the default items argument, they end up sharing the same list. Adding an item to one object unintentionally alters the list for the other. The following example shows the standard pattern for safely initializing mutable attributes.

class TodoList:
   def __init__(self, name, items=None):
       self.name = name
       self.items = items if items is not None else []
   
   def add_item(self, item):
       self.items.append(item)

list1 = TodoList("Work")
list1.add_item("Finish project")
list2 = TodoList("Personal")
list2.add_item("Buy groceries")
print(list1.items)
print(list2.items)

The corrected code solves the problem by setting the default for items to None. The constructor then uses the expression items if items is not None else [] to create a brand new empty list for each object that doesn't receive one. This ensures every instance starts with its own unique list, so modifications to one don't accidentally affect another. It's a crucial pattern to remember whenever you use mutable types as default arguments.

Inheritance and forgetting to call parent __init__

In inheritance, if a child class defines its own __init__, it overrides the parent's. You must explicitly call the parent's constructor, or its setup logic won't run. This leaves the object partially uninitialized, often causing errors later on.

The following example shows what happens when the Dog class forgets to initialize its Animal parent, leading to a missing attribute.

class Animal:
   def __init__(self, species):
       self.species = species

class Dog(Animal):
   def __init__(self, name, breed):
       self.name = name
       self.breed = breed
   
   def info(self):
       return f"{self.name} is a {self.species} of breed {self.breed}"

dog = Dog("Rex", "German Shepherd")
print(dog.info())

The Dog class's __init__ method never calls its parent's constructor, so the species attribute is never assigned. This triggers an AttributeError when the info() method tries to access self.species. The corrected implementation follows.

class Animal:
   def __init__(self, species):
       self.species = species

class Dog(Animal):
   def __init__(self, name, breed):
       super().__init__("dog")
       self.name = name
       self.breed = breed
   
   def info(self):
       return f"{self.name} is a {self.species} of breed {self.breed}"

dog = Dog("Rex", "German Shepherd")
print(dog.info())

The corrected code calls the parent's constructor using super().__init__("dog") at the start of the child's __init__ method. This simple line runs the Animal class's setup logic, correctly assigning the species attribute. You'll need to do this whenever a subclass defines its own __init__, ensuring the object inherits all necessary attributes from its parent and preventing AttributeError exceptions down the line.

Real-world applications

With those common errors handled, you can see how constructors are used to build practical features, from a simple shopping cart to a singleton.

Creating a shopping cart with __init__

A shopping cart provides a classic example of how the __init__ constructor sets up an object's initial state, giving it a customer ID and an empty list to store items.

class ShoppingCart:
   def __init__(self, customer_id):
       self.customer_id = customer_id
       self.items = []
       
   def add_item(self, product, price, quantity=1):
       self.items.append({"product": product, "price": price, "quantity": quantity})
           
cart = ShoppingCart("cust123")
cart.add_item("Laptop", 1000)
cart.add_item("Mouse", 25, 2)
print(f"Cart: {cart.items}")

When you create a ShoppingCart object, its __init__ method assigns the provided customer_id and prepares an empty items list. This ensures every cart starts fresh and is tied to a specific customer, preventing data from mixing.

  • The add_item method bundles product details into a dictionary before appending it to that specific cart's list.
  • Notice how the quantity parameter has a default value, making it optional when you call the method.

This design keeps each cart's contents neatly organized and separate from others.

Implementing a singleton pattern with __new__ and __init__

While __init__ initializes an object, the singleton pattern uses the more fundamental __new__ method to ensure only one instance of a class is ever created.

class DatabaseConnection:
   _instance = None
   
   def __new__(cls, host, user):
       if cls._instance is None:
           cls._instance = super().__new__(cls)
           cls._instance.host = host
           cls._instance.user = user
       return cls._instance

db1 = DatabaseConnection("localhost", "user1")
db2 = DatabaseConnection("otherhost", "user2")
print(f"Same instance: {db1 is db2}, Host: {db1.host}")

This code uses the special __new__ method to control object creation itself, a process that runs before __init__. Its goal is to enforce that only one DatabaseConnection object can ever exist in the application.

  • The first time you instantiate the class, it creates and returns a new object.
  • On any subsequent attempt, it bypasses creation and simply returns the original object.

This pattern ensures all parts of your application share a single, consistent connection point, preventing multiple connections from being opened unnecessarily.

Get started with Replit

Now, put these constructor patterns to work. Try prompting Replit Agent with: “Build a Python class for a simple Stopwatch” or “Create a Character class for a game with stats like health and mana.”

The agent takes your description and writes the code, tests for errors, and handles deployment. Start building with Replit.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started for free

Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.