Learn › Python Programming

Object-Oriented Programming

Build classes and objects in Python, understanding constructors, methods, inheritance, and the use of super().

Classes and Objects

Classes in Python are blueprints for creating objects that bundle data and functionality together. The class keyword defines a new class, and calling the class like a function creates a new instance. Instance attributes are typically set in the __init__ method, which serves as the constructor. Python uses self as a reference to the current instance, passed explicitly as the first parameter to instance methods.

# Defining a class
class Dog:
    species = "Canis familiaris"  # Class attribute

    def __init__(self, name, age):
        self.name = name          # Instance attribute
        self.age = age

    def bark(self):
        return f"{self.name} says Woof!"

    def __str__(self):
        return f"{self.name} ({self.age} years old)"

# Creating instances
buddy = Dog("Buddy", 5)
max_dog = Dog("Max", 3)

print(buddy.bark())    # "Buddy says Woof!"
print(str(max_dog))    # "Max (3 years old)"
print(Dog.species)     # "Canis familiaris"

The __init__ Method and Encapsulation

The __init__ method is called automatically when a new instance is created and is used to initialize the object's state. Python does not have strict access modifiers like private or protected, but uses naming conventions to indicate intended access levels. A single underscore prefix indicates a protected attribute, while a double underscore prefix triggers name mangling to prevent accidental access. Properties provide a Pythonic way to implement getters and setters.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance  # Name-mangled attribute

    @property
    def balance(self):
        return self.__balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self.__balance += amount
        return self.__balance

    def withdraw(self, amount):
        if amount > self.__balance:
            raise ValueError("Insufficient funds")
        self.__balance -= amount
        return self.__balance

    def __repr__(self):
        return f"BankAccount('{self.owner}', {self.__balance})"

account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account.balance)  # 1300
print(account)          # BankAccount('Alice', 1300)

Inheritance

Inheritance allows a class to derive attributes and methods from a parent class, promoting code reuse and establishing a hierarchy. The child class can override parent methods to provide specialized behavior while keeping the same interface. Python supports multiple inheritance, where a class can inherit from more than one parent. The Method Resolution Order (MRO) determines which method is called when multiple parents define the same method.

class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

    def speak(self):
        return f"{self.name} says {self.sound}!"

    def info(self):
        return f"{self.name} is an animal"

class Cat(Animal):
    def __init__(self, name, indoor=True):
        super().__init__(name, "Meow")
        self.indoor = indoor

    def info(self):
        location = "indoor" if self.indoor else "outdoor"
        return f"{self.name} is an {location} cat"

class Kitten(Cat):
    def __init__(self, name, indoor=True):
        super().__init__(name, indoor)

    def speak(self):
        return f"{self.name} says Mew! (tiny meow)"

whiskers = Cat("Whiskers")
print(whiskers.speak())  # "Whiskers says Meow!"
print(whiskers.info())   # "Whiskers is an indoor cat"

tiny = Kitten("Tiny")
print(tiny.speak())      # "Tiny says Mew! (tiny meow)"

Using super() and Method Resolution

The super() function returns a proxy object that delegates method calls to a parent or sibling class in the MRO. It is essential for cooperative multiple inheritance, ensuring each class in the hierarchy is initialized properly. Using super() instead of directly calling the parent class makes your code more maintainable and compatible with complex inheritance trees. The MRO can be inspected using the __mro__ attribute or the mro() method on any class.

class Shape:
    def __init__(self, color):
        self.color = color

    def area(self):
        raise NotImplementedError("Subclasses must implement area()")

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, color, side):
        super().__init__(color, side, side)

# Usage
rect = Rectangle("blue", 5, 3)
print(f"Area: {rect.area()}, Color: {rect.color}")  # Area: 15, Color: blue

sq = Square("red", 4)
print(f"Area: {sq.area()}, Color: {sq.color}")       # Area: 16, Color: red

# Inspect Method Resolution Order
print(Square.__mro__)
# (<class 'Square'>, <class 'Rectangle'>, <class 'Shape'>, <class 'object'>)

← Lists, Tuples & Dicts · File Handling →