Object-Oriented Programming

Python Classes and Objects

Learn object-oriented programming in Python with classes, objects, constructors, attributes, methods, and special methods.

Python Classes and Objects

Object-Oriented Programming (OOP) is a programming paradigm that organizes code into classes and objects. A class is a blueprint for creating objects, and an object is an instance of a class.


Defining a Class

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

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

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

    # Another method
    def info(self):
        return f"{self.name} is {self.age} years old"

Creating Objects

python
# Create instances (objects)
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

# Access attributes
print(dog1.name)      # Buddy
print(dog2.age)       # 5
print(dog1.species)   # Canis familiaris

# Call methods
print(dog1.bark())    # Buddy says Woof!
print(dog2.info())    # Max is 5 years old

# Modify attributes
dog1.age = 4
print(dog1.info())    # Buddy is 4 years old

# Each object is independent
print(dog1 is dog2)   # False

The __init__ Method

The constructor initializes new objects:

python
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.transactions = []

    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            self.transactions.append(f"+${amount:.2f}")
            return True
        return False

    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance -= amount
            self.transactions.append(f"-${amount:.2f}")
            return True
        return False

    def get_statement(self):
        print(f"Account: {self.owner}")
        print(f"Balance: ${self.balance:.2f}")
        print(f"Transactions: {', '.join(self.transactions)}")

# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
account.get_statement()
# Account: Alice
# Balance: $1300.00
# Transactions: +$500.00, -$200.00

The self Parameter

self refers to the current instance. It's automatically passed when calling methods:

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

    def distance_to(self, other):
        """Calculate distance to another point."""
        dx = self.x - other.x
        dy = self.y - other.y
        return (dx**2 + dy**2) ** 0.5

p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance_to(p2))  # 5.0

# Equivalent explicit call (rarely used)
print(Point.distance_to(p1, p2))  # 5.0

Class vs Instance Attributes

python
class Student:
    # Class attribute - shared by ALL instances
    school = "BigXStar Academy"
    student_count = 0

    def __init__(self, name):
        # Instance attribute - unique to each instance
        self.name = name
        Student.student_count += 1

    def __repr__(self):
        return f"Student('{self.name}')"

s1 = Student("Alice")
s2 = Student("Bob")

# Class attribute - same for all
print(s1.school)             # BigXStar Academy
print(s2.school)             # BigXStar Academy
print(Student.student_count) # 2

# Modifying class attribute via class
Student.school = "Tech Academy"
print(s1.school)  # Tech Academy (changed for all)
print(s2.school)  # Tech Academy

# Modifying via instance creates a new instance attribute
s1.school = "Private School"
print(s1.school)         # Private School (instance attribute)
print(s2.school)         # Tech Academy (still class attribute)
print(Student.school)    # Tech Academy

Special (Magic/Dunder) Methods

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

    # String representation (for users)
    def __str__(self):
        return f"({self.x}, {self.y})"

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

    # Addition: v1 + v2
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    # Subtraction: v1 - v2
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    # Equality: v1 == v2
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

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

    # Boolean (non-zero vector)
    def __bool__(self):
        return self.x != 0 or self.y != 0

    # Length for len()
    def __len__(self):
        return 2  # 2D vector

v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1)           # (3, 4)
print(repr(v1))     # Vector(3, 4)
print(v1 + v2)      # (4, 6)
print(v1 - v2)      # (2, 2)
print(v1 == v2)     # False
print(abs(v1))      # 5.0
print(bool(Vector(0, 0)))  # False

Common Magic Methods

MethodOperator/Function
__init__Constructor
__str__str(), print()
__repr__repr(), debugging
__add__+
__sub__-
__mul__*
__eq__==
__lt__<
__len__len()
__getitem__obj[key]
__setitem__obj[key] = val
__contains__in
__iter__for x in obj
__call__obj()

Class Methods and Static Methods

python
class Employee:
    raise_rate = 1.05  # 5% raise

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    # Regular instance method
    def apply_raise(self):
        self.salary = int(self.salary * self.raise_rate)

    # Class method - receives class as first argument
    @classmethod
    def set_raise_rate(cls, rate):
        cls.raise_rate = rate

    # Class method as alternative constructor
    @classmethod
    def from_string(cls, emp_str):
        name, salary = emp_str.split("-")
        return cls(name, int(salary))

    # Static method - no access to instance or class
    @staticmethod
    def is_workday(day):
        return day.weekday() < 5

# Class method
Employee.set_raise_rate(1.10)

# Alternative constructor
emp = Employee.from_string("Alice-75000")
print(f"{emp.name}: ${emp.salary}")  # Alice: $75000

# Static method
import datetime
today = datetime.date.today()
print(Employee.is_workday(today))

Properties

Control attribute access with getters and setters:

python
class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius

    @property
    def celsius(self):
        """Get temperature in Celsius."""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """Set temperature with validation."""
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        self._celsius = value

    @property
    def fahrenheit(self):
        """Get temperature in Fahrenheit."""
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

# Usage
temp = Temperature(25)
print(temp.celsius)      # 25
print(temp.fahrenheit)   # 77.0

temp.fahrenheit = 212
print(temp.celsius)      # 100.0

# Validation works
# temp.celsius = -300  # ValueError!

Practical Example: Library System

python
"""
A simple library management system using classes.
"""

class Book:
    def __init__(self, title, author, isbn):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.is_available = True

    def __str__(self):
        status = "Available" if self.is_available else "Checked Out"
        return f"'{self.title}' by {self.author} [{status}]"

    def __repr__(self):
        return f"Book('{self.title}', '{self.author}', '{self.isbn}')"


class Library:
    def __init__(self, name):
        self.name = name
        self.books = []

    def add_book(self, book):
        self.books.append(book)
        print(f"Added: {book.title}")

    def find_book(self, title):
        for book in self.books:
            if book.title.lower() == title.lower():
                return book
        return None

    def checkout(self, title):
        book = self.find_book(title)
        if book and book.is_available:
            book.is_available = False
            print(f"Checked out: {book.title}")
            return True
        print(f"Cannot checkout: {title}")
        return False

    def return_book(self, title):
        book = self.find_book(title)
        if book and not book.is_available:
            book.is_available = True
            print(f"Returned: {book.title}")

    def display_catalog(self):
        print(f"\nπŸ“š {self.name} Catalog:")
        print("-" * 40)
        for book in self.books:
            print(f"  {book}")

# Usage
library = Library("City Library")
library.add_book(Book("Python Crash Course", "Eric Matthes", "978-1"))
library.add_book(Book("Clean Code", "Robert Martin", "978-2"))
library.add_book(Book("The Pragmatic Programmer", "Hunt & Thomas", "978-3"))

library.display_catalog()
library.checkout("Clean Code")
library.display_catalog()
library.return_book("Clean Code")

Summary

  • A class is a blueprint; an object is an instance of a class
  • __init__ is the constructor that initializes new objects
  • self refers to the current instance
  • Class attributes are shared by all instances; instance attributes are unique
  • Magic methods (__str__, __add__, etc.) enable operator overloading
  • @classmethod receives the class; @staticmethod receives neither class nor instance
  • @property provides controlled access to attributes with getters/setters
  • OOP helps organize code into reusable, modular components

Next, we'll learn about inheritance in Python.