PYTHON • OBJECT-ORIENTED PROGRAMMING

__init__ & Constructors — Use __init__ for initialization and constructors

Learn how Python's __init__ method sets up every object the moment it's created.

Historical Context & Motivation

Before programming languages had object-oriented programming (OOP), developers wrote code as long sequences of instructions with little structure. As software grew more complex in the 1960s and 1970s, programmers needed a way to bundle data and behavior together into reusable units. The concept of a constructor — a special piece of code that runs automatically when an object is created — became essential to this new paradigm.

1967
Simula Introduces Objects
The Simula language, created in Norway by Ole-Johan Dahl and Kristen Nygaard, introduced classes and objects. It included early constructor-like mechanisms to initialize objects upon creation.
1983
C++ Formalizes Constructors
Bjarne Stroustrup's C++ language gave constructors a formal role. A constructor shared the class's name and ran automatically, setting up each new object with its starting data.
1991
Python Is Born
Guido van Rossum released Python with a clear, readable approach to OOP. Python used the special method __init__ as its initializer, keeping constructor logic simple and consistent.
2000s
Python Becomes Mainstream
As Python gained popularity in education, web development, and data science, __init__ became one of the first OOP concepts every new Python programmer learned.

The central question this lesson addresses is: how does a Python object get its starting data the moment it's created? Without a reliable way to set initial values, every programmer would have to manually assign attributes after creating an object — leading to bugs, forgotten steps, and messy code. The __init__ method solves this problem elegantly.

Core Principles & Definitions

In Python, a class is a blueprint for creating objects, and an object (also called an instance) is a specific item built from that blueprint. The __init__ method is the special function inside a class that Python calls automatically every time you create a new instance. It lets you give each object its own personalized starting data, known as instance attributes.

1

Class

A template or blueprint that defines what attributes and methods an object will have. Think of it like a cookie cutter that shapes every cookie the same way.
2

__init__ Method

A special method (also called a dunder method, short for "double underscore") that Python runs automatically when a new object is created. It initializes the object's attributes.
3

self Parameter

The first parameter of __init__ (and every instance method). It refers to the specific object being created, allowing you to attach attributes to that particular instance.
4

Instance Attributes

Variables that belong to a specific object. They are set inside __init__ using the pattern self.attribute_name = value, giving each object its own data.
5

Constructor vs. Initializer

Technically, __new__ creates the object (constructor) and __init__ sets it up (initializer). In practice, most Python programmers call __init__ the constructor because __new__ is rarely overridden.
✦ KEY TAKEAWAY
Think of __init__ like a registration form you fill out when you sign up for a new account online. The moment you click "Create Account," the system automatically collects your name, email, and password and stores them in your profile. You don't have to go back later and add each field one by one — it all happens at creation time. That's exactly what __init__ does for Python objects.

Visual Explanation

This diagram shows the three-step flow: you define a class with __init__ (purple box), call the class like a function (gold box), Python automatically runs __init__ (cyan box), and a fully initialized object is produced (green box).

The diagram above illustrates a critical point: you never call __init__ directly. Instead, you call the class name as if it were a function — like Dog("Rex", "Lab") — and Python handles the rest behind the scenes. First, Python creates a blank object using __new__, then it passes that blank object as self to your __init__ method along with any arguments you provided. By the time __init__ finishes, the object has all its data and is ready to use.

How __init__ Works Under the Hood

When you write my_dog = Dog("Rex", "Lab"), Python executes a precise sequence of steps internally. Understanding this mechanism helps you debug problems and write cleaner classes.

The Object Creation Sequence

  1. Step 1 — Call the class: Python sees Dog("Rex", "Lab") and recognizes you are asking to create a new Dog instance.
  2. Step 2 — __new__ creates a blank object: Python's built-in __new__ method allocates memory and creates an empty object. You almost never override this.
  3. Step 3 — __init__ initializes: Python calls __init__(self, "Rex", "Lab"), where self is the blank object. Inside __init__, you attach data using self.name = name.
  4. Step 4 — Return the object: The fully initialized object is returned and stored in your variable my_dog. Note that __init__ must never return a value — it should always return None (which it does by default).

The self Parameter Explained

The parameter self is what connects your __init__ code to the specific object being created. Every time you write self.something = value, you're saying "attach this data to this particular object." This is why two Dog objects can have different names — each one has its own self during initialization. The word self is a strong convention in Python, not a keyword. You could technically name it anything, but you should always use self to keep your code readable and consistent with the Python community.

⚠️ Common Mistake
Forgetting self as the first parameter of __init__ is one of the most common beginner errors. If you write def __init__(name, breed): instead of def __init__(self, name, breed):, Python will assign the object itself to name and crash with a TypeError about missing arguments.

Common __init__ Patterns

Once you understand the basics of __init__, you'll encounter several common patterns in real-world Python code. These patterns demonstrate the flexibility of constructors and how they can handle different situations.

Four common patterns for __init__: (1) required parameters that must always be provided, (2) default parameters with fallback values, (3) computed attributes calculated from inputs, and (4) validation that checks data before storing it.

Pattern 1 (required parameters) is the most straightforward and what you'll use most often as a beginner. Pattern 2 (default parameters) is extremely useful when some attributes have sensible fallback values. Notice how default values go at the end of the parameter list, just like with any Python function. Pattern 3 (computed attributes) shows that you can create attributes inside __init__ that aren't directly passed as arguments — they're derived from the inputs. Pattern 4 (validation) demonstrates one of the most powerful uses of __init__: ensuring that objects are always created with valid data.

Worked Example: Building a BankAccount Class

Let's build a complete class step by step, demonstrating how __init__ fits into a realistic example. We'll create a BankAccount class that initializes with an owner name and an optional starting balance.

Building a BankAccount Class
1
Step 1 — Define the Class and __init__We start by creating the class and defining its __init__ method. The owner is required, but the balance defaults to 0 if not provided. class BankAccount: def __init__(self, owner, balance=0): self.owner = owner self.balance = balance
Class defined with two instance attributes: owner and balance
2
Step 2 — Add ValidationA bank account shouldn't start with a negative balance. We add a check inside __init__ to prevent invalid data. class BankAccount: def __init__(self, owner, balance=0): if balance < 0: raise ValueError("Balance cannot be negative") self.owner = owner self.balance = balance
Invalid data is caught immediately at object creation
3
Step 3 — Add Methods That Use the AttributesNow we add deposit and withdraw methods. These methods access self.balance — the attribute that was set up by __init__. def deposit(self, amount): self.balance += amount return self.balance def withdraw(self, amount): if amount > self.balance: print("Insufficient funds!") else: self.balance -= amount return self.balance
Methods rely on the attributes __init__ created
4
Step 4 — Create and Use ObjectsNow we create two separate accounts. Each one gets its own owner and balance thanks to __init__. acct1 = BankAccount("Alice", 500) acct2 = BankAccount("Bob") print(acct1.owner) # Alice print(acct1.balance) # 500 print(acct2.balance) # 0 (used default) acct1.deposit(200) print(acct1.balance) # 700
Alice's account starts with 500, Bob's starts with 0 (default). Each object is independent.
✦ KEY TAKEAWAY
The __init__ method is like the foundation of a house. Before you can add rooms (methods), furniture (data manipulation), or decorations (string representations), you need a solid foundation that defines the structure. Every other method in your class will rely on the attributes that __init__ sets up.

With vs. Without __init__

To appreciate why __init__ matters, let's compare code that uses it against code that doesn't. You can technically create a class without __init__ and add attributes manually after the object is created, but this approach is error-prone and hard to maintain.

Comparison of manual attribute assignment vs. using __init__
FeatureWithout __init__With __init__
Attribute setupMust manually set each attribute after creation: dog.name = "Rex"All attributes set automatically at creation: Dog("Rex")
ConsistencyEasy to forget an attribute, leading to AttributeError laterEvery object is guaranteed to have the same set of attributes
ValidationNo automatic validation — bad data can slip in unnoticedCan check data immediately and raise errors before the object is used
ReadabilityAttributes scattered across the codebaseOne place to see all attributes — the __init__ method
Lines of codeMore lines overall, repeated for each objectFewer lines — constructor handles setup once
💡 BEST PRACTICE
Always define an __init__ method in your classes, even if it only sets one or two attributes. This ensures that every object starts life in a known, predictable state. Professional Python developers consider a class without __init__ to be a red flag in code reviews.

Connection to Advanced OOP Concepts

The __init__ method is your gateway to more advanced object-oriented programming concepts. As you progress, you'll see how this foundational idea connects to inheritance, polymorphism, and design patterns.

How __init__ connects to advanced topics
What You Know NowWhere It Leads
Writing __init__ in a single classInheritance: Child classes can extend or override a parent's __init__ using super().__init__()
Setting instance attributes with self.x = xProperties: Use @property decorators to add getter/setter logic around attributes initialized in __init__
Using __init__ as the constructor__new__ vs. __init__: Learn the true constructor (__new__) for advanced patterns like singletons and immutable objects
Validating data inside __init__Dataclasses: Python's @dataclass decorator auto-generates __init__ from type annotations, reducing boilerplate

When you study inheritance, you'll learn that a child class can have its own __init__ that calls the parent's __init__ using super().__init__(). This lets you build on existing classes without rewriting all the setup code. For example, a SavingsAccount could inherit from BankAccount and add an interest_rate attribute while reusing the parent's owner and balance setup. These advanced topics build directly on the __init__ skills you're learning now.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words why self is needed as the first parameter of __init__. What would happen if you created two objects from the same class without self?
PROBLEM 2 — BASIC CALCULATION
Write a class called Circle with an __init__ that takes a radius and also computes and stores the area as an attribute (use π ≈ 3.14159). Then create a Circle with radius 5 and access its area.
PROBLEM 3 — INTERMEDIATE
Create a class called Playlist whose __init__ takes a name (required) and initializes an empty list called songs. Add an add_song method that appends a song title to the list, and a count method that returns the number of songs. Why is it important that songs is initialized as self.songs = [] inside __init__ rather than as a class variable?
PROBLEM 4 — APPLIED
Design a GradeBook class for a teacher. The __init__ should take course_name and max_students (default 30). It should initialize an empty dictionary called students and validate that max_students is between 1 and 40. Write the complete __init__ and an enroll method that adds a student name with an empty list of grades, but only if the class isn't full.
PROBLEM 5 — CRITICAL THINKING
Consider the following code: class Pet: tricks = [] def __init__(self, name): self.name = name def add_trick(self, trick): self.tricks.append(trick) dog = Pet("Buddy") cat = Pet("Whiskers") dog.add_trick("roll over") print(cat.tricks) What does print(cat.tricks) output, and why? How would you fix this class so each pet has its own independent tricks list?

Lesson Summary

The __init__ method is Python's initializer — it runs automatically every time you create a new instance of a class. Inside __init__, you use the self parameter to attach instance attributes to the specific object being created. You never call __init__ directly — Python calls it for you when you write something like Dog("Rex", "Lab").

Key patterns include required parameters, default parameters, computed attributes, and validation inside __init__. Always initialize mutable objects like lists and dictionaries inside __init__ (not at the class level) so each object gets its own copy. As you advance, __init__ connects directly to inheritance with super().__init__() and to modern tools like dataclasses.

Varsity Tutors • Python • __init__ & Constructors