Skip to content
Python by Example

Object-Oriented Programming

Constructors and Initialization in Python

08_oop/02_init_methods.py

This file explores the init method and other initialization patterns in Python classes. The init method is called when creating new objects and sets up the initial state of the instance.

"""
================================================================================
File: 02_init_methods.py
Topic: Constructors and Initialization in Python
================================================================================
 
This file explores the __init__ method and other initialization patterns
in Python classes. The __init__ method is called when creating new objects
and sets up the initial state of the instance.
 
Key Concepts:
- The __init__ constructor
- Required vs optional parameters
- Default values
- Property decorators
- Alternative constructors (classmethod)
 
================================================================================
"""
 
# -----------------------------------------------------------------------------
# 1. Basic __init__ Method
# -----------------------------------------------------------------------------
# __init__ initializes newly created objects
 
print("--- Basic __init__ ---")
 
class Book:
    """A class representing a book."""
    
    def __init__(self, title, author):
        """
        Initialize a new book.
        
        Args:
            title: The book's title
            author: The book's author
        """
        self.title = title
        self.author = author
 
# Creating instances calls __init__ automatically
book = Book("1984", "George Orwell")
print(f"Book: '{book.title}' by {book.author}")
 
# Without proper arguments, you get an error:
# book = Book()  # TypeError: missing 2 required positional arguments
 
# -----------------------------------------------------------------------------
# 2. Default Parameter Values
# -----------------------------------------------------------------------------
# Make some parameters optional
 
print("\n--- Default Values ---")
 
class User:
    """A class with default parameter values."""
    
    def __init__(self, username, email=None, role="user", active=True):
        """
        Initialize a new user.
        
        Args:
            username: Required username
            email: Optional email address
            role: User role (default: "user")
            active: Is user active (default: True)
        """
        self.username = username
        self.email = email
        self.role = role
        self.active = active
    
    def __str__(self):
        email = self.email or "No email"
        status = "Active" if self.active else "Inactive"
        return f"{self.username} ({self.role}) - {email} - {status}"
 
# Various ways to create users
user1 = User("alice")
user2 = User("bob", "bob@example.com")
user3 = User("charlie", "charlie@example.com", "admin")
user4 = User("dave", active=False)
 
print(user1)
print(user2)
print(user3)
print(user4)
 
# -----------------------------------------------------------------------------
# 3. Validation in __init__
# -----------------------------------------------------------------------------
# Validate and process data during initialization
 
print("\n--- Validation in __init__ ---")
 
class Temperature:
    """A class with validation in __init__."""
    
    def __init__(self, celsius):
        """
        Initialize temperature in Celsius.
        
        Args:
            celsius: Temperature in Celsius
            
        Raises:
            ValueError: If temperature is below absolute zero
        """
        if celsius < -273.15:
            raise ValueError("Temperature cannot be below absolute zero!")
        self._celsius = celsius
    
    @property
    def celsius(self):
        """Get temperature in Celsius."""
        return self._celsius
    
    @property
    def fahrenheit(self):
        """Get temperature in Fahrenheit."""
        return (self._celsius * 9/5) + 32
    
    @property
    def kelvin(self):
        """Get temperature in Kelvin."""
        return self._celsius + 273.15
 
temp = Temperature(25)
print(f"Temperature: {temp.celsius}°C = {temp.fahrenheit}°F = {temp.kelvin}K")
 
# Validation in action
try:
    invalid_temp = Temperature(-300)
except ValueError as e:
    print(f"Validation error: {e}")
 
# -----------------------------------------------------------------------------
# 4. Mutable Default Arguments Warning
# -----------------------------------------------------------------------------
# Never use mutable objects as default arguments!
 
print("\n--- Mutable Defaults Warning ---")
 
# BAD - mutable default argument
class BadStudent:
    def __init__(self, name, grades=[]):  # DON'T DO THIS!
        self.name = name
        self.grades = grades
 
s1 = BadStudent("Alice")
s1.grades.append(90)
 
s2 = BadStudent("Bob")
print(f"Bob's grades (should be empty): {s2.grades}")  # Contains 90!
 
# GOOD - use None and create new list
class GoodStudent:
    def __init__(self, name, grades=None):
        self.name = name
        self.grades = grades if grades is not None else []
 
g1 = GoodStudent("Carol")
g1.grades.append(85)
 
g2 = GoodStudent("Dan")
print(f"Dan's grades (correctly empty): {g2.grades}")
 
# -----------------------------------------------------------------------------
# 5. Property Decorators
# -----------------------------------------------------------------------------
# Control attribute access with getters and setters
 
print("\n--- Property Decorators ---")
 
class Circle:
    """A circle with property decorators."""
    
    def __init__(self, radius):
        """Initialize circle with radius."""
        self._radius = radius  # Protected attribute
    
    @property
    def radius(self):
        """Get the radius."""
        return self._radius
    
    @radius.setter
    def radius(self, value):
        """Set the radius with validation."""
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value
    
    @property
    def diameter(self):
        """Get the diameter (read-only computed property)."""
        return self._radius * 2
    
    @property
    def area(self):
        """Get the area (read-only computed property)."""
        import math
        return math.pi * self._radius ** 2
 
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Diameter: {circle.diameter}")
print(f"Area: {circle.area:.2f}")
 
# Using setter
circle.radius = 10
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area:.2f}")
 
# Validation in setter
try:
    circle.radius = -5
except ValueError as e:
    print(f"Setter validation: {e}")
 
# -----------------------------------------------------------------------------
# 6. Alternative Constructors with @classmethod
# -----------------------------------------------------------------------------
# Create objects in different ways
 
print("\n--- Alternative Constructors ---")
 
class Person:
    """A class with alternative constructors."""
    
    def __init__(self, first_name, last_name, age):
        """Standard constructor."""
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
    
    @classmethod
    def from_birth_year(cls, first_name, last_name, birth_year):
        """Create person from birth year instead of age."""
        import datetime
        age = datetime.datetime.now().year - birth_year
        return cls(first_name, last_name, age)
    
    @classmethod
    def from_full_name(cls, full_name, age):
        """Create person from full name string."""
        parts = full_name.split()
        first_name = parts[0]
        last_name = parts[-1] if len(parts) > 1 else ""
        return cls(first_name, last_name, age)
    
    @classmethod
    def from_dict(cls, data):
        """Create person from dictionary."""
        return cls(
            data.get('first_name', ''),
            data.get('last_name', ''),
            data.get('age', 0)
        )
    
    def __str__(self):
        return f"{self.first_name} {self.last_name}, age {self.age}"
 
# Using different constructors
p1 = Person("John", "Doe", 30)
p2 = Person.from_birth_year("Jane", "Smith", 1995)
p3 = Person.from_full_name("Bob Johnson", 25)
p4 = Person.from_dict({"first_name": "Alice", "last_name": "Williams", "age": 28})
 
print("Created using different constructors:")
print(f"  Standard: {p1}")
print(f"  From birth year: {p2}")
print(f"  From full name: {p3}")
print(f"  From dict: {p4}")
 
# -----------------------------------------------------------------------------
# 7. __new__ vs __init__
# -----------------------------------------------------------------------------
# __new__ creates the instance, __init__ initializes it
 
print("\n--- __new__ vs __init__ ---")
 
class Singleton:
    """A singleton class using __new__."""
    
    _instance = None
    
    def __new__(cls):
        """Create instance only if one doesn't exist."""
        if cls._instance is None:
            print("  Creating new instance...")
            cls._instance = super().__new__(cls)
        else:
            print("  Returning existing instance...")
        return cls._instance
    
    def __init__(self):
        """Initialize (called every time)."""
        pass
 
# Both variables point to the same instance
print("Creating s1:")
s1 = Singleton()
print("Creating s2:")
s2 = Singleton()
print(f"Same instance? {s1 is s2}")
 
# -----------------------------------------------------------------------------
# 8. Initialization with Inheritance
# -----------------------------------------------------------------------------
 
print("\n--- Initialization with Inheritance ---")
 
class Animal:
    """Base class for animals."""
    
    def __init__(self, name, species):
        """Initialize animal."""
        self.name = name
        self.species = species
    
    def speak(self):
        """Make a sound."""
        return "Some sound"
 
class Dog(Animal):
    """Dog class inheriting from Animal."""
    
    def __init__(self, name, breed):
        """Initialize dog with name and breed."""
        # Call parent's __init__
        super().__init__(name, species="Canis familiaris")
        self.breed = breed
    
    def speak(self):
        """Dogs bark."""
        return "Woof!"
 
class Cat(Animal):
    """Cat class inheriting from Animal."""
    
    def __init__(self, name, indoor=True):
        """Initialize cat."""
        super().__init__(name, species="Felis catus")
        self.indoor = indoor
    
    def speak(self):
        """Cats meow."""
        return "Meow!"
 
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", indoor=True)
 
print(f"Dog: {dog.name} ({dog.breed}) says {dog.speak()}")
print(f"Cat: {cat.name} (Indoor: {cat.indoor}) says {cat.speak()}")
 
# -----------------------------------------------------------------------------
# 9. Complex Initialization Example
# -----------------------------------------------------------------------------
 
print("\n--- Complex Initialization ---")
 
from datetime import datetime
 
class Order:
    """A class with complex initialization."""
    
    _order_counter = 0
    
    def __init__(self, customer_name, items=None, discount=0):
        """
        Initialize an order.
        
        Args:
            customer_name: Name of the customer
            items: List of (item_name, price, quantity) tuples
            discount: Discount percentage (0-100)
        """
        # Auto-generate order ID
        Order._order_counter += 1
        self.order_id = f"ORD-{Order._order_counter:04d}"
        
        # Set basic attributes
        self.customer_name = customer_name
        self.items = items or []
        self.discount = max(0, min(100, discount))  # Clamp to 0-100
        
        # Auto-generated attributes
        self.created_at = datetime.now()
        self._subtotal = None  # Cached calculation
    
    def add_item(self, name, price, quantity=1):
        """Add an item to the order."""
        self.items.append((name, price, quantity))
        self._subtotal = None  # Invalidate cache
    
    @property
    def subtotal(self):
        """Calculate subtotal."""
        if self._subtotal is None:
            self._subtotal = sum(price * qty for _, price, qty in self.items)
        return self._subtotal
    
    @property
    def total(self):
        """Calculate total with discount."""
        return self.subtotal * (1 - self.discount / 100)
    
    def __str__(self):
        return f"Order {self.order_id} for {self.customer_name}: ${self.total:.2f}"
 
# Create orders
order1 = Order("Alice")
order1.add_item("Widget", 9.99, 2)
order1.add_item("Gadget", 14.99, 1)
 
order2 = Order("Bob", discount=10)
order2.add_item("Premium Widget", 29.99, 1)
 
print(order1)
print(f"  Subtotal: ${order1.subtotal:.2f}")
print(f"  Total: ${order1.total:.2f}")
 
print(order2)
print(f"  Subtotal: ${order2.subtotal:.2f}")
print(f"  Total (10% off): ${order2.total:.2f}")
 
# -----------------------------------------------------------------------------
# 10. Summary
# -----------------------------------------------------------------------------
 
print("\n--- Summary ---")
 
summary = """
Initialization patterns:
  - __init__: Main constructor, initializes attributes
  - Default values: Make parameters optional
  - Validation: Raise errors for invalid input
  - Properties: Computed/validated attributes
  - @classmethod: Alternative constructors
  - __new__: Instance creation (before __init__)
  - super().__init__(): Call parent constructor
"""
 
print(summary)