Skip to main content
Python OOP

Object-Oriented Programming (OOP)

In Python, everything is an object. Functions are objects, numbers are objects, even classes themselves are objects. OOP allows you to create your own custom types to model your problem domain.

1. Classes & Objects

A Class is the blueprint. An Object is the instance.
class Dog:
    # Class Attribute: Shared by ALL instances of Dog
    species = "Canis familiaris"

    # Initializer (Constructor)
    # 'self' refers to the specific object being created
    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!"

# Usage
buddy = Dog("Buddy", 5)
print(buddy.bark())

The self Parameter

In Python, you must explicitly define self as the first parameter of instance methods. It’s how the method knows which object it’s operating on. (It’s similar to this in Java/C++, but explicit).

2. Inheritance

Inheritance allows you to create specialized versions of existing classes.
class Animal:
    def speak(self):
        pass # Placeholder

class Cat(Animal):
    def speak(self):
        return "Meow"

class Dog(Animal):
    def speak(self):
        return "Woof"

super()

Use super() to call methods from the parent class. This is useful when you want to extend behavior rather than replace it.
class GoldenRetriever(Dog):
    def speak(self):
        # Call parent's speak(), then add to it
        return super().speak() + " (Golden Style)"

3. Magic Methods (Dunder Methods)

Python classes can integrate tightly with language syntax using “Magic Methods” (Double UNDERscore methods).
  • __init__: Constructor.
  • __str__: String representation (for print()).
  • __add__: Defines behavior for + operator.
  • __eq__: Defines behavior for == operator.
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # Called when you do print(v)
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

    # Called when you do v1 + v2
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

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

print(v3) # Output: Vector(4, 6)

4. Properties

In Java, you write getVariable() and setVariable(). In Python, we prefer direct access (obj.variable). But what if you need validation? Use the @property decorator. It lets you use a method as if it were an attribute.
class Circle:
    def __init__(self, radius):
        self._radius = radius # Convention: _variable means "internal use only"

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

c = Circle(5)
c.radius = 10 # Calls the setter method!
# c.radius = -1 # Raises ValueError

5. Dataclasses (Python 3.7+)

If you are writing a class just to hold data (like a struct), standard classes are verbose. Dataclasses automate the boilerplate (__init__, __repr__, __eq__).
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int
    z: int = 0 # Default value

p1 = Point(1, 2)
p2 = Point(1, 2)

print(p1)       # Output: Point(x=1, y=2, z=0)
print(p1 == p2) # Output: True (Compares values, not memory addresses)

Summary

  • Classes: Encapsulate data and behavior.
  • Inheritance: Reuse code.
  • Magic Methods: Make your objects behave like built-in types (+, ==, print).
  • Properties: Add logic to attribute access without changing the API.
  • Dataclasses: The modern, concise way to define data containers.
Next, we’ll learn how to organize code into Modules and Packages.