Python Object-Oriented Programming

Python supports classes, inheritance, polymorphism, and encapsulation. OOP helps organize code into reusable, logical structures.

Classes and Objects

Python
class Dog:
    """A simple Dog class."""
    
    # Class attribute (shared by all instances)
    species = "Canis familiaris"
    
    def __init__(self, name, age, breed):
        """Initialize a new Dog instance."""
        self.name = name      # Instance attribute
        self.age = age
        self.breed = breed
        self._tricks = []     # Convention: _ prefix = "private"
    
    def bark(self):
        """Make the dog bark."""
        return f"{self.name} says Woof!"
    
    def learn_trick(self, trick):
        self._tricks.append(trick)
    
    def show_tricks(self):
        return f"{self.name} knows: {', '.join(self._tricks)}"
    
    def __str__(self):
        """String representation."""
        return f"{self.name} ({self.breed}, {self.age} years old)"
    
    def __repr__(self):
        """Developer representation."""
        return f"Dog(name='{self.name}', age={self.age}, breed='{self.breed}')"

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

print(buddy)              # Buddy (Golden Retriever, 3 years old)
print(buddy.bark())       # Buddy says Woof!
buddy.learn_trick("sit")
buddy.learn_trick("shake")
print(buddy.show_tricks())  # Buddy knows: sit, shake

Inheritance

Python
class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound
    
    def speak(self):
        return f"{self.name} says {self.sound}!"
    
    def __str__(self):
        return f"{self.__class__.__name__}: {self.name}"

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, sound="Woof")  # Call parent constructor
        self.breed = breed
    
    def fetch(self, item):
        return f"{self.name} fetches the {item}!"

class Cat(Animal):
    def __init__(self, name, indoor=True):
        super().__init__(name, sound="Meow")
        self.indoor = indoor
    
    def describe(self):
        location = "indoor" if self.indoor else "outdoor"
        return f"{self.name} is an {location} cat"

# Polymorphism — same method, different behavior
animals = [Dog("Rex", "Labrador"), Cat("Whiskers")]
for animal in animals:
    print(animal.speak())  # Each animal speaks differently!
# Rex says Woof!
# Whiskers says Meow!

Properties

Python
class Circle:
    def __init__(self, radius):
        self._radius = radius  # "Private" attribute
    
    @property
    def radius(self):
        """Getter — access like an attribute."""
        return self._radius
    
    @radius.setter
    def radius(self, value):
        """Setter — validate before setting."""
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value
    
    @property
    def area(self):
        """Computed property (read-only)."""
        import math
        return math.pi * self._radius ** 2
    
    @property
    def circumference(self):
        import math
        return 2 * math.pi * self._radius

c = Circle(5)
print(c.radius)           # 5 (calls getter)
print(c.area)             # 78.539... (computed on access)
c.radius = 10             # Calls setter
# c.area = 100            # AttributeError (read-only)
# c.radius = -1           # ValueError: Radius cannot be negative

Dataclasses (Python 3.7+)

Python
from dataclasses import dataclass, field
from typing import List

@dataclass
class User:
    name: str
    email: str
    age: int
    hobbies: List[str] = field(default_factory=list)
    
    @property
    def is_adult(self) -> bool:
        return self.age >= 18
    
    def greet(self) -> str:
        return f"Hi, I'm {self.name}!"

# Auto-generates: __init__, __repr__, __eq__, and more
alice = User("Alice", "alice@example.com", 28, ["reading", "coding"])
bob = User("Bob", "bob@example.com", 25)

print(alice)          # User(name='Alice', email='alice@example.com', age=28, hobbies=['reading', 'coding'])
print(alice == bob)   # False (structural equality)

# Copy with modifications
older_alice = alice.replace(age=29)

Dunder Methods

Python
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 __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 __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)    # Vector(4, 6)
print(v1 * 3)     # Vector(3, 6)
print(abs(v1))    # 2.236...

Knowledge Check

4 questions — test your understanding

1

What does __init__ do in a Python class?

2

What does super().__init__() do?

3

What is the @property decorator used for?

4

What does a dataclass automatically generate?