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
@propertyfor computed or read-only attributes and to add validation without changing the public API. @classmethodreceives the class as first argument (useful for alternative constructors);@staticmethodhas 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.