advanced Step 14 of 20

OOP - Classes

Python Programming

OOP - Classes

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects — instances of classes that bundle data (attributes) and behavior (methods) together. OOP helps you model real-world entities, encapsulate complexity, and create reusable code components. Python is a multi-paradigm language that fully supports OOP with a clean and intuitive syntax. Understanding classes is essential for working with frameworks like Django, Flask, and virtually every Python library.

Defining a Class

# Basic class definition
class Dog:
    # Class attribute (shared by all instances)
    species = "Canis familiaris"

    # Constructor (initializer)
    def __init__(self, name, age, breed):
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age
        self.breed = breed

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

    # Instance method
    def description(self):
        return f"{self.name} is a {self.age}-year-old {self.breed}"

    # String representation
    def __str__(self):
        return f"Dog({self.name}, {self.breed})"

    def __repr__(self):
        return f"Dog(name='{self.name}', age={self.age}, breed='{self.breed}')"

# Creating instances (objects)
buddy = Dog("Buddy", 3, "Golden Retriever")
max_dog = Dog("Max", 5, "German Shepherd")

print(buddy.name)          # "Buddy"
print(buddy.bark())        # "Buddy says Woof!"
print(buddy.description()) # "Buddy is a 3-year-old Golden Retriever"
print(buddy.species)       # "Canis familiaris"
print(str(buddy))          # "Dog(Buddy, Golden Retriever)"

Properties and Encapsulation

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance    # Convention: _ prefix = "private"
        self._transactions = []

    @property
    def balance(self):
        """Read-only access to balance."""
        return self._balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive")
        self._balance += amount
        self._transactions.append(f"+${amount:.2f}")
        return self._balance

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive")
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        self._transactions.append(f"-${amount:.2f}")
        return self._balance

    @property
    def statement(self):
        return " | ".join(self._transactions)

    def __str__(self):
        return f"Account({self.owner}: ${self._balance:.2f})"

# Usage
acct = BankAccount("Alice", 1000)
acct.deposit(500)
acct.withdraw(200)
print(acct.balance)    # 1300 (accessed as property, not method call)
print(acct.statement)  # "+$500.00 | -$200.00"
# acct.balance = 999   # AttributeError — read-only property

Class Methods and Static Methods

class Employee:
    raise_percentage = 1.04    # 4% raise
    employee_count = 0

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
        Employee.employee_count += 1

    def apply_raise(self):
        self.salary *= self.raise_percentage

    @classmethod
    def set_raise_percentage(cls, percentage):
        """Modify class-level attribute."""
        cls.raise_percentage = percentage

    @classmethod
    def from_string(cls, employee_str):
        """Alternative constructor from 'name-salary' string."""
        name, salary = employee_str.split("-")
        return cls(name, float(salary))

    @staticmethod
    def is_workday(day):
        """Utility method — no access to cls or self."""
        return day.weekday() < 5

# Regular method — operates on instance (self)
emp = Employee("Alice", 80000)
emp.apply_raise()
print(emp.salary)  # 83200.0

# Class method — operates on class (cls)
Employee.set_raise_percentage(1.05)
emp2 = Employee.from_string("Bob-75000")
print(emp2.name, emp2.salary)  # Bob 75000.0

# Static method — no access to instance or class
from datetime import date
today = date.today()
print(Employee.is_workday(today))

print(Employee.employee_count)  # 2

Dunder (Magic) Methods

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __abs__(self):
        return (self.x**2 + self.y**2) ** 0.5

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __len__(self):
        return 2

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)     # Vector(4, 6)
print(v1 - v2)     # Vector(2, 2)
print(v1 * 3)      # Vector(9, 12)
print(abs(v1))     # 5.0
print(v1 == v2)    # False
Pro tip: Use the @property decorator to create computed attributes and enforce validation. Start with simple public attributes and add properties later if you need validation or computation — Python's property mechanism means you can do this without changing the API for users of your class.

Key Takeaways

  • Classes bundle data (attributes) and behavior (methods); use __init__ to initialize instance attributes.
  • Use @property for computed or read-only attributes and to add validation without changing the public API.
  • @classmethod receives the class as first argument (useful for alternative constructors); @staticmethod has no access to instance or class.
  • Dunder methods (__add__, __str__, __eq__, etc.) let your objects work with Python's built-in operators and functions.
  • Python uses naming conventions (single underscore _) rather than access modifiers for encapsulation.