What is __init__ in Python?

Learn about the __init__ method in Python: the class constructor that initializes objects. Syntax, practical examples and best practices for using it properly.
10 min read
Believemy logo

In Python, the __init__ method is one of the most fundamental special methods in the language.

Called automatically when an object is created, it allows you to initialize the attributes of a class instance. If you want to master object-oriented programming in Python, understanding __init__ is absolutely essential.

Whether you are a beginner or an experienced developer, our comprehensive Python course guides you step by step through learning these essential concepts.

 

Definition of __init__ in Python

The __init__ method is a special method (also called a dunder method, for "double underscore") that serves as a constructor in Python. Its role is to initialize a new object right after its creation in memory.

Concretely, when you create an instance of a class with MyClass(), Python performs two operations behind the scenes:

  1. __new__: creates the object in memory (rarely overridden).
  2. __init__: initializes the object with the values you pass to it.
Good to know

Technically, __init__ is not a "constructor" in the strict sense (it is __new__ that creates the object), but it is called so by convention because it is the one that configures the object for use.

The basic syntax is as follows:

PYTHON
class MyClass:
    def __init__(self):
        # Initialize attributes
        self.attribute = value

The first parameter of __init__ is always self, which represents the instance being created. You can then add as many additional parameters as needed.

 

Syntax and parameters of __init__

The __init__ method is defined inside a class using the def keyword. Here is its complete syntax with different types of parameters:

__init__ with simple parameters

PYTHON
class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age

# Creating an instance
user = User("Alice", "alice@example.com", 30)
print(user.name)   # Alice
print(user.email)  # alice@example.com
print(user.age)    # 30

In this example, name, email and age are required parameters. If you try to create a User without providing them, Python will raise a TypeError.

 

__init__ with default values

You can define default values for certain parameters, making them optional:

PYTHON
class User:
    def __init__(self, name, email, age=18, role="member"):
        self.name = name
        self.email = email
        self.age = age
        self.role = role

# With default values
user1 = User("Bob", "bob@example.com")
print(user1.age)   # 18
print(user1.role)  # member

# Overriding default values
user2 = User("Charlie", "charlie@example.com", age=25, role="admin")
print(user2.age)   # 25
print(user2.role)  # admin

 

__init__ with *args and **kwargs

For maximum flexibility, you can use *args and **kwargs:

PYTHON
class ServerConfig:
    def __init__(self, host, port, **options):
        self.host = host
        self.port = port
        self.options = options

server = ServerConfig("localhost", 8080, debug=True, timeout=30)
print(server.options)  # {'debug': True, 'timeout': 30}

 

Practical examples and use cases

Let us now look at concrete and progressive examples to understand how __init__ is used in real-world situations.

Initialization with data validation

A very common use of __init__ is to validate data as soon as the object is created:

PYTHON
class BankAccount:
    def __init__(self, holder, initial_balance=0):
        if not isinstance(holder, str) or len(holder) == 0:
            raise ValueError("The holder must be a non-empty string.")
        if initial_balance < 0:
            raise ValueError("The initial balance cannot be negative.")
        
        self.holder = holder
        self.balance = initial_balance
        self.history = []
    
    def deposit(self, amount):
        self.balance += amount
        self.history.append(f"Deposit: +${amount}")
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Insufficient balance.")
        self.balance -= amount
        self.history.append(f"Withdrawal: -${amount}")

# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account.balance)  # 1300
print(account.history)  # ['Deposit: +$500', 'Withdrawal: -$200']
Warning

Warning: never modify attributes directly that should be protected. Use properties (@property) to control access if needed.

 

Initialization with computed attributes

The __init__ method can also compute attributes from the received parameters:

PYTHON
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)
    
    def __repr__(self):
        return f"Rectangle({self.width}, {self.height})"

rect = Rectangle(10, 5)
print(rect.area)       # 50
print(rect.perimeter)  # 30
print(rect)            # Rectangle(10, 5)

 

Inheritance and calling the parent __init__ with super()

When you create a class that inherits from another, it is essential to call the parent's __init__ so that the object is correctly initialized:

PYTHON
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
        self.alive = True

class Dog(Animal):
    def __init__(self, name, breed, age):
        super().__init__(name, species="Dog")
        self.breed = breed
        self.age = age
    
    def bark(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, species="Cat")
        self.color = color
    
    def meow(self):
        return f"{self.name} says Meow!"

rex = Dog("Rex", "German Shepherd", 5)
print(rex.name)     # Rex
print(rex.species)  # Dog
print(rex.breed)    # German Shepherd
print(rex.bark())   # Rex says Woof!

felix = Cat("Felix", "black")
print(felix.meow())  # Felix says Meow!
Good to know

The super() function allows you to call methods from the parent class. This is the recommended way to handle inheritance in Python, rather than calling Animal.__init__(self, ...) directly.

 

Comparison with dataclasses

Python offers dataclass which automatically generate the __init__ method for you. Let's see the difference:

PYTHON
from dataclasses import dataclass

# Without dataclass: manual __init__
class ClassicProduct:
    def __init__(self, name, price, stock=0):
        self.name = name
        self.price = price
        self.stock = stock

# With dataclass: automatically generated __init__
@dataclass
class DataclassProduct:
    name: str
    price: float
    stock: int = 0

# Both work the same way
p1 = ClassicProduct("Keyboard", 49.99, 10)
p2 = DataclassProduct("Keyboard", 49.99, 10)

print(p1.name, p1.price)  # Keyboard 49.99
print(p2.name, p2.price)  # Keyboard 49.99
CriteriaManual __init__@dataclass
FlexibilityFull (custom logic)Limited (via __post_init__)
VerbosityMore code to writeConcise and readable
Auto-generated methodsNone__repr__, __eq__, etc.
Ideal use caseComplex initialization logicSimple data objects

 

Best practices for __init__

Here are the essential rules for writing good __init__ methods in Python:

1. Keep __init__ simple and focused

The role of __init__ is to initialize the object, not to perform complex operations. If your constructor is more than 15-20 lines long, it is probably a sign that you need to refactor:

PYTHON
# ❌ Bad practice: too much logic in __init__
class SalesReport:
    def __init__(self, file):
        self.file = file
        self.data = self._load_file(file)  # heavy operation
        self.total = sum(d['amount'] for d in self.data)
        self.average = self.total / len(self.data)
        self._send_notification()  # side effect

# ✅ Good practice: simple __init__, separate methods
class SalesReport:
    def __init__(self, file):
        self.file = file
        self.data = []
        self.total = 0
        self.average = 0
    
    def load(self):
        self.data = self._load_file(self.file)
        self.total = sum(d['amount'] for d in self.data)
        self.average = self.total / len(self.data)
        return self

 

2. Initialize all attributes in __init__

All instance attributes should be defined in __init__, even if they are initialized to None. This makes your code more readable and facilitates the docstring:

PYTHON
class Connection:
    def __init__(self, url, timeout=30):
        self.url = url
        self.timeout = timeout
        self.session = None        # Will be initialized later
        self.is_connected = False  # Explicit initial state
        self.last_error = None

 

3. Avoid mutable objects as default values

This is one of the most classic pitfalls in Python:

PYTHON
# ❌ DANGER: the list is shared between all instances!
class Team:
    def __init__(self, name, members=[]):
        self.name = name
        self.members = members

t1 = Team("Alpha")
t1.members.append("Alice")

t2 = Team("Beta")
print(t2.members)  # ['Alice'] — Surprise!

# ✅ Good practice: use None and create a new list
class Team:
    def __init__(self, name, members=None):
        self.name = name
        self.members = members if members is not None else []
Warning

Mutable objects (list, dict, set) used as default values are shared between all instances. Always use None as the default value, then create a new object in the body of __init__.

 

4. Use type annotations

Type annotations improve readability and allow static analysis tools to check your code:

PYTHON
from typing import Optional

class Article:
    def __init__(self, title: str, content: str, author: str, tags: Optional[list[str]] = None) -> None:
        self.title = title
        self.content = content
        self.author = author
        self.tags = tags if tags is not None else []

 

5. Use alternative class methods

If you need multiple ways to create an object, use class methods with @classmethod rather than overloading __init__:

PYTHON
import json

class Configuration:
    def __init__(self, host: str, port: int, debug: bool = False):
        self.host = host
        self.port = port
        self.debug = debug
    
    @classmethod
    def from_json(cls, file_path: str):
        with open(file_path) as f:
            data = json.load(f)
        return cls(data['host'], data['port'], data.get('debug', False))
    
    @classmethod
    def default(cls):
        return cls('localhost', 8080, debug=True)

# Different ways to create the object
config1 = Configuration("192.168.1.1", 3000)
config2 = Configuration.from_json("config.json")
config3 = Configuration.default()

 

The __init__ method is part of a larger ecosystem of special methods in Python. Here are the most important ones to know in relation to initialization:

MethodRoleWhen is it called?
__new__Creates the instance in memoryBefore __init__
__init__Initializes the instanceRight after __new__
__post_init__Post-initialization (dataclasses)After auto-generated __init__
__del__Cleanup before destructionWhen the object is destroyed
__repr__Developer representationrepr(object)
__str__User representationstr(object) / print

Here is an example combining __init__ with other special methods:

PYTHON
class Vector:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __str__(self):
        return f"({self.x}, {self.y})"
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    def __len__(self):
        return int((self.x ** 2 + self.y ** 2) ** 0.5)

v1 = Vector(3, 4)
v2 = Vector(1, 2)
v3 = v1 + v2

print(v3)          # (4, 6)
print(repr(v3))    # Vector(4, 6)
print(v1 == v2)    # False

 

Frequently asked questions

Question

What is the difference between __init__ and __new__ in Python?

__new__ is responsible for creating the instance in memory. It receives the class (cls) as its first parameter and returns a new object. __init__ is then called to initialize that object with attributes. In 99% of cases, you only need __init__. __new__ is only useful in advanced cases such as singletons or immutable classes (for example, to subclass tuple or str).

 

Question

Why must self always be the first parameter of __init__?

self is a Python convention that refers to the instance being created. When you write object = MyClass(args), Python internally calls MyClass.__init__(object, args). The self parameter therefore automatically receives the reference to the object. Although you could technically use a different name, self is a universal convention in Python and not following it would make your code very difficult to read.

 

Question

Can __init__ return a value?

No. The __init__ method must never return anything other than None. If you try to return a value, Python will raise a TypeError: __init__() should return None error. Its role is solely to initialize the object, not to create or return anything. If you need to control what is returned during creation, it is __new__ that you need to override.

 

Question

How can I learn to master __init__ and OOP in Python?

To properly understand __init__ and all of object-oriented programming in Python, nothing beats a structured and progressive approach. Our dedicated Python course on Believemy guides you from discovering the basics to advanced concepts like inheritance, design patterns and special methods. You will find practical exercises and real-world projects to consolidate your skills.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.