Constructors — Default, Parameterized, Copy
Introduction
In the previous tutorial, we learned that a class is a blueprint and an object is a concrete instance created from it. But we glossed over a critical question: when you create an object, how does it get its initial state?
When you write BankAccount("Alice", 5000.0), something has to assign "Alice" to the owner attribute and 5000.0 to balance. That "something" is the constructor — a special method that runs automatically every time a new object is created.
Constructors are the gatekeepers of object creation. They decide:
- What data is required to create a valid object (e.g., every bank account needs an owner)
- What data is optional with sensible defaults (e.g., balance defaults to 0)
- What validation happens at creation time (e.g., balance can't be negative)
Understanding constructors is essential because they're the first line of defense for data integrity. A well-designed constructor makes it impossible to create an invalid object — a concept we'll see become even more powerful with encapsulation (the next tutorial).
What is a Constructor?
A constructor is a special method that:
- Runs automatically when you create a new object — you never call it directly
- Initializes the object's state — sets up the attributes with their starting values
- Has the same name as the class (in Java) or is named
__init__(in Python) - Does not return a value — its only job is to set up the object
Think of a constructor like the intake process at a hospital. When a patient arrives, the admissions team collects their name, date of birth, insurance information, and assigns a room. The patient can't "exist" in the hospital system without going through this process first. That's what a constructor does for objects.
There are three main types of constructors, and we'll explore each one.
![Three constructor types. Default: no args, fills preset values (ShoppingCart() -> items=[], total=0.0) - use when a blank start is valid. Parameterized: takes required data and validates it (User("alice","a@x.com") checks username not empty, email has @, role valid) - most common, reject bad data at birth. Copy: builds a new object from an existing one (GameCharacter.copy(hero)) for save/undo/testing. Deep-copy trap: self.inventory = other.inventory shares one list (mutating one corrupts the other) vs self.inventory = list(other.inventory) which makes a new independent list.](https://pub-a1b3030acfb94e84ba8a89fb182c53bc.r2.dev/public/lld-content-fdf7102f-4e85-460f-9330-1663551c3480/constructors-three-types.webp)
Default Constructor — The Zero-Argument Setup
A default constructor takes no parameters (beyond self/this) and sets attributes to predefined default values.
When to use it: When it's reasonable for an object to start in a "blank" or "default" state — for example, a shopping cart starts empty, or a counter starts at zero.
class ShoppingCart:
"""A shopping cart with default (empty) initial state."""
def __init__(self):
# Default constructor — no parameters needed
self.items: list[str] = [] # starts empty
self.total: float = 0.0 # starts at zero
self.is_checked_out: bool = False
def add_item(self, item: str, price: float) -> None:
self.items.append(item)
self.total += price
print(f"Added '{item}' (${price:.2f}). Cart total: ${self.total:.2f}")
# Creating objects with the default constructor — no arguments needed
cart1 = ShoppingCart() # Empty cart
cart2 = ShoppingCart() # Another empty cart (independent)
cart1.add_item("Keyboard", 89.99)
cart1.add_item("Mouse", 29.99)
print(f"Cart 1 items: {len(cart1.items)}") # 2
print(f"Cart 2 items: {len(cart2.items)}") # 0 — independent!
# Output:
# Added 'Keyboard' ($89.99). Cart total: $89.99
# Added 'Mouse' ($29.99). Cart total: $119.98
# Cart 1 items: 2
# Cart 2 items: 0import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
private List<String> items;
private double total;
private boolean isCheckedOut;
// Default constructor — no parameters
public ShoppingCart() {
this.items = new ArrayList<>();
this.total = 0.0;
this.isCheckedOut = false;
}
public void addItem(String item, double price) {
items.add(item);
total += price;
System.out.printf("Added '%s' ($%.2f). Cart total: $%.2f%n", item, price, total);
}
public int getItemCount() {
return items.size();
}
public static void main(String[] args) {
ShoppingCart cart1 = new ShoppingCart();
ShoppingCart cart2 = new ShoppingCart();
cart1.addItem("Keyboard", 89.99);
cart1.addItem("Mouse", 29.99);
System.out.println("Cart 1 items: " + cart1.getItemCount()); // 2
System.out.println("Cart 2 items: " + cart2.getItemCount()); // 0
}
}Important Python note: If you don't define any __init__ method in Python, Python provides an implicit default constructor that does nothing. But this means your attributes won't be initialized, which leads to AttributeError bugs. Always define __init__ explicitly.
Important Java note: If you don't write any constructor in Java, the compiler automatically generates a no-argument constructor that initializes fields to their defaults (null for objects, 0 for numbers, false for booleans). But the moment you write any constructor, this automatic one goes away.
Parameterized Constructor — Custom Initial State
A parameterized constructor takes arguments that let you set the object's initial state at creation time. This is the most common type of constructor.
When to use it: When the object needs specific data to be valid — a user needs a name and email, a bank account needs an owner.
class User:
"""A user account that requires specific data at creation."""
def __init__(self, username: str, email: str, role: str = "member"):
"""Parameterized constructor with validation.
Args:
username: Required — can't create a user without it
email: Required — can't create a user without it
role: Optional — defaults to 'member' if not provided
"""
# Validation — reject invalid data at creation time
if not username or not username.strip():
raise ValueError("Username cannot be empty")
if "@" not in email:
raise ValueError(f"Invalid email: {email}")
if role not in ("member", "admin", "moderator"):
raise ValueError(f"Invalid role: {role}")
self.username = username.strip()
self.email = email.strip().lower()
self.role = role
self.is_active = True # Always starts as active
def __repr__(self) -> str:
return f"User('{self.username}', '{self.email}', role='{self.role}')"
# Using the parameterized constructor
alice = User("alice_dev", "alice@example.com") # default role
bob = User("bob_admin", "bob@example.com", role="admin") # custom role
print(alice) # User('alice_dev', 'alice@example.com', role='member')
print(bob) # User('bob_admin', 'bob@example.com', role='admin')
# Validation catches bad data at creation time:
try:
bad_user = User("", "no-email") # ValueError: Username cannot be empty
except ValueError as e:
print(f"Caught: {e}")
# Output:
# User('alice_dev', 'alice@example.com', role='member')
# User('bob_admin', 'bob@example.com', role='admin')
# Caught: Username cannot be emptypublic class User {
private String username;
private String email;
private String role;
private boolean isActive;
// Parameterized constructor with validation
public User(String username, String email, String role) {
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + email);
}
if (!role.equals("member") && !role.equals("admin") && !role.equals("moderator")) {
throw new IllegalArgumentException("Invalid role: " + role);
}
this.username = username.trim();
this.email = email.trim().toLowerCase();
this.role = role;
this.isActive = true; // Always starts as active
}
// Overloaded constructor — default role
public User(String username, String email) {
this(username, email, "member"); // Delegates to the full constructor
}
@Override
public String toString() {
return String.format("User('%s', '%s', role='%s')", username, email, role);
}
public static void main(String[] args) {
User alice = new User("alice_dev", "alice@example.com");
User bob = new User("bob_admin", "bob@example.com", "admin");
System.out.println(alice); // User('alice_dev', 'alice@example.com', role='member')
System.out.println(bob); // User('bob_admin', 'bob@example.com', role='admin')
try {
User bad = new User("", "no-email");
} catch (IllegalArgumentException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}Notice something powerful: by putting validation in the constructor, we make it impossible to create an invalid User object. An empty username or a malformed email is rejected immediately. This is the first step toward what we'll call encapsulation in the next tutorial — protecting an object's integrity from the moment it's born.
Copy Constructor — Cloning an Object
A copy constructor creates a new object by copying the state from an existing object. The new object is an independent copy — changing one doesn't affect the other.
When to use it: When you need a snapshot of an object's current state — for undo functionality, testing, caching, or creating variations of existing objects.
Real-world analogy: Photocopying a document. The copy looks identical, but it's a separate piece of paper — marking up the copy doesn't change the original.
class GameCharacter:
"""A game character with a copy constructor for saving/cloning state."""
def __init__(self, name: str, health: int, level: int,
inventory: list[str] | None = None):
self.name = name
self.health = health
self.level = level
# CRITICAL: create a NEW list, not a reference to the same one
self.inventory = list(inventory) if inventory else []
@classmethod
def copy(cls, other: 'GameCharacter') -> 'GameCharacter':
"""Copy constructor — creates an independent clone.
Uses @classmethod since Python doesn't support constructor overloading.
"""
return cls(
name=other.name,
health=other.health,
level=other.level,
inventory=list(other.inventory) # Deep copy the list!
)
def take_damage(self, amount: int) -> None:
self.health = max(0, self.health - amount)
def pick_up(self, item: str) -> None:
self.inventory.append(item)
def __repr__(self) -> str:
return (f"GameCharacter('{self.name}', hp={self.health}, "
f"lv={self.level}, items={self.inventory})")
# Create original character
hero = GameCharacter("Aria", health=100, level=5, inventory=["Sword", "Shield"])
print(f"Original: {hero}")
# Save a snapshot using copy constructor
saved_state = GameCharacter.copy(hero)
# Hero takes damage and picks up items
hero.take_damage(40)
hero.pick_up("Potion")
print(f"After battle: {hero}")
print(f"Saved state: {saved_state}") # Unaffected!
# Output:
# Original: GameCharacter('Aria', hp=100, lv=5, items=['Sword', 'Shield'])
# After battle: GameCharacter('Aria', hp=60, lv=5, items=['Sword', 'Shield', 'Potion'])
# Saved state: GameCharacter('Aria', hp=100, lv=5, items=['Sword', 'Shield'])import java.util.ArrayList;
import java.util.List;
public class GameCharacter {
private String name;
private int health;
private int level;
private List<String> inventory;
// Regular constructor
public GameCharacter(String name, int health, int level, List<String> inventory) {
this.name = name;
this.health = health;
this.level = level;
// CRITICAL: create a NEW list, not a reference to the same one
this.inventory = new ArrayList<>(inventory);
}
// Copy constructor — takes an existing object and clones it
public GameCharacter(GameCharacter other) {
this.name = other.name;
this.health = other.health;
this.level = other.level;
this.inventory = new ArrayList<>(other.inventory); // Deep copy!
}
public void takeDamage(int amount) {
this.health = Math.max(0, this.health - amount);
}
public void pickUp(String item) {
this.inventory.add(item);
}
@Override
public String toString() {
return String.format("GameCharacter('%s', hp=%d, lv=%d, items=%s)",
name, health, level, inventory);
}
public static void main(String[] args) {
GameCharacter hero = new GameCharacter(
"Aria", 100, 5, List.of("Sword", "Shield"));
System.out.println("Original: " + hero);
// Save a snapshot using copy constructor
GameCharacter savedState = new GameCharacter(hero);
// Hero takes damage and picks up items
hero.takeDamage(40);
hero.pickUp("Potion");
System.out.println("After battle: " + hero);
System.out.println("Saved state: " + savedState); // Unaffected!
}
}The Deep Copy Trap
Notice how both implementations create a new list for the inventory: list(other.inventory) in Python and new ArrayList<>(other.inventory) in Java. This is critical.
If you wrote self.inventory = other.inventory instead, both objects would share the same list in memory. Picking up an item in the original would also appear in the copy — defeating the entire purpose.
This distinction between shallow copy (copy the reference) and deep copy (copy the actual data) is one of the most common sources of bugs in OOP. The rule: if an attribute is mutable (lists, maps, sets), always create a new copy in the constructor.
We'll encounter this concept again when we study the Prototype Pattern in Section 9 (Creational Patterns) — a design pattern specifically about cloning objects.
Visualization

Constructor Overloading (Java) and Default Parameters (Python)
Often, you want to offer multiple ways to create an object — sometimes with all data specified, sometimes with just the essentials. Python and Java handle this differently:
Java: Constructor Overloading
Java lets you define multiple constructors with different parameter lists. The compiler picks the right one based on the arguments you pass:
public class Rectangle {
private double width;
private double height;
// Constructor 1: both dimensions specified
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
// Constructor 2: square (same width and height)
public Rectangle(double side) {
this(side, side); // Delegates to Constructor 1
}
// Constructor 3: default 1x1 rectangle
public Rectangle() {
this(1.0, 1.0); // Delegates to Constructor 1
}
}
Notice the this(...) call — this is constructor chaining, where one constructor calls another to avoid duplicating initialization logic.
Python: Default Parameters
Python doesn't support overloading (only one __init__), but default parameters achieve the same result:
class Rectangle:
def __init__(self, width: float = 1.0, height: float | None = None):
self.width = width
self.height = height if height is not None else width
# All three work:
rect1 = Rectangle(10.0, 5.0) # 10 x 5
rect2 = Rectangle(7.0) # 7 x 7 (square)
rect3 = Rectangle() # 1 x 1 (default)
For more complex cases in Python, you can use @classmethod to create named alternative constructors (as we saw with GameCharacter.copy()).
Common Mistakes
Mistake 1: No Validation in Constructor
❌ Wrong: Accepting any data without checking, leading to invalid objects that break later.
class User:
def __init__(self, email):
self.email = email # What if email is empty? Or not an email at all?
✅ Right: Validate in the constructor. Catch problems at creation time, not when the object is used.
Mistake 2: Forgetting Deep Copy for Mutable Attributes
❌ Wrong: Sharing the same list reference between the original and the copy.
def __init__(self, other):
self.inventory = other.inventory # SAME list! Not a copy!
✅ Right: Always create a new collection when copying.
def __init__(self, other):
self.inventory = list(other.inventory) # NEW list with same items
Mistake 3: Doing Too Much in the Constructor
❌ Wrong: Making API calls, reading files, or starting threads inside a constructor. This makes the class hard to test and can cause surprising failures.
✅ Right: The constructor should only initialize state. Complex setup belongs in a separate method:
user = User("alice", "alice@example.com") # Fast, predictable
user.load_profile_from_api() # Separate step, easy to test/mock
Key Takeaways
- A constructor is a special method that runs automatically when an object is created — it sets up the initial state
- Default constructors take no parameters and set attributes to predefined values — use when a blank/empty start is valid
- Parameterized constructors require specific data and should validate it — making it impossible to create invalid objects
- Copy constructors create independent clones — essential for save/restore, undo, and testing. Always deep copy mutable attributes
- Java supports constructor overloading (multiple constructors) and constructor chaining (
this()). Python uses default parameters and@classmethodalternatives - Keep constructors focused on initialization — put complex setup logic in separate methods
- In the next tutorial, we'll learn encapsulation — how to protect an object's data from unauthorized access and ensure it can only be changed through controlled methods