Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

Python

The Complete Guide to Python Object-Oriented Programming

Learn Python OOP by understanding when and why to use classes, inheritance, properties, and magic methods. Practical examples show you how to organize complexity without over-engineering.

June 2026 · 12 min read · 2 views · 0 hearts

The Complete Guide to Python Object-Oriented Programming

Most tutorials treat OOP like a classroom lecture. Let's fix that.

Python's object-oriented programming isn't about memorizing syntax — it's about organizing complexity. By the end of this guide, you'll not only understand classes and inheritance but know when and why to use them.

Classes Are Blueprints, Objects Are Buildings

Think of a class as a cookie cutter. The object is the actual cookie.

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

my_car = Car("Tesla", "Model 3", 2024)

Simple, right? But here's what matters: __init__ is the constructor that runs when you create an object. The self parameter refers to the instance itself — without it, your methods won't know which object they're working on.

Attributes vs Methods: Objects

Objects have two things: - Attributes (data): like my_car.make - Methods (functions): things the object can do

class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
        self.mileage = 0

    def drive(self, miles):
        self.mileage += miles
        print(f"Driven {miles} miles. Total: {self.mileage}")

Notice how drive() modifies self.mileage — that's encapsulation in action: the object manages its own state.

Inheritance: Don't Repeat Yourself

The biggest win of OOP is avoiding copied code. If you have multiple types of vehicles, inheritance saves you.

class Vehicle:
    def __init__(self, wheels):
        self.wheels = wheels

    def start(self):
        print("Vroom!")

class Motorcycle(Vehicle):
    def __init__(self):
        super().__init__(2)  # Motorcycles have 2 wheels

class Truck(Vehicle):
    def __init__(self):
        super().__init__(4)  # Trucks have 4 wheels

bike = Motorcycle()
bike.start()  # Output: Vroom!

The parent class Vehicle handles the shared logic. The child classes only define what's different. super() is how you access parent methods from within the child.

Method Overriding: Customize Behavior

Sometimes you need to change how a parent method works. That's overriding.

class ElectricCar(Car):
    def drive(self, miles):
        self.mileage += miles
        print(f"Silently drove {miles} miles. Total: {self.mileage}")

    def recharge(self):
        print("Charging battery...")

The ElectricCar overrides drive() to add its own message. It also adds a new method recharge(). Inheritance gives you a base, but overriding lets you customize.

Private Attributes: Python's Convention

Python doesn't have true private variables like Java or C++. Instead, it uses underscores as a convention.

class BankAccount:
    def __init__(self, balance):
        self._balance = balance  # "protected" by convention
        self.__pin = "1234"      # name mangling: actually _BankAccount__pin

    def get_balance(self):
        return self._balance
  • Single underscore _: It's meant for internal use. Programmers know not to touch it, but Python won't stop them.
  • Double underscore __: Python renames the attribute to _ClassName__attribute. This prevents accidental overriding in subclasses.

Real talk: Most Python developers use single underscore. Double underscores are rare and often overkill.

Properties: Getters and Setters Done Right

Don't write get_value() and set_value() methods like in Java. Python has @property.

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

    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32

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

temp = Temperature(0)
print(temp.fahrenheit)  # 32.0
temp.fahrenheit = 100
print(temp._celsius)    # 37.78

Properties look like attributes but behave like methods. They let you add logic later without breaking existing code that accesses temp.fahrenheit.

Class Methods and Static Methods

Not every method needs a specific instance.

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    @classmethod
    def margherita(cls):
        return cls(["mozzarella", "tomatoes", "basil"])

    @staticmethod
    def oven_temperature():
        return 425  # Fahrenheit

pizza = Pizza.margherita()  # Class method: creates object
print(Pizza.oven_temperature())  # Static method: no object needed
  • Class methods (@classmethod): Receive the class itself (cls) and can create instances. Perfect for alternative constructors.
  • Static methods (@staticmethod): Work like regular functions but live in the class namespace. Use them for utility logic related to the class.

Magic Methods: Double Underscore Power

__init__ is just the tip of the iceberg. Magic methods let objects work with Python's built-in operations.

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __str__(self):
        return f"'{self.title}'"

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

    def __len__(self):
        return self.pages

    def __eq__(self, other):
        return self.title == other.title

book = Book("Python 101", 300)
print(book)       # Uses __str__: 'Python 101'
print(repr(book)) # Uses __repr__: Book('Python 101', 300)
print(len(book))  # 300

The most useful magic methods: - __str__: Human-readable string - __repr__: Unambiguous representation (for debugging) - __len__: For len(obj) - __eq__: For obj1 == obj2

Composition Over Inheritance

Here's a trap beginners fall into: using inheritance for everything. Often composition is cleaner.

# Inheritance (sometimes makes sense)
class Dog(Animal):
    pass

# Composition (often better)
class Car:
    def __init__(self):
        self.engine = Engine()
        self.wheels = [Wheel() for _ in range(4)]

A car has an engine and wheels (composition). It is not an engine (inheritance). Favor composition when the relationship is "has-a" rather than "is-a".

When to Actually Use OOP

OOP isn't always the answer. Use it when:

  • You have multiple objects that share behavior (inheritance wins)
  • You need to bundle data with operations on that data (encapsulation)
  • Your codebase is growing and you need structure

Skip it when:

  • You're writing a small script
  • Your data is just a simple dictionary
  • You're doing heavy number crunching (functional programming often works better)

Real-World Pitfalls

Deep inheritance chains: Having A -> B -> C -> D is a nightmare. Keep inheritance at most 2–3 levels deep.

Mutable shared state: Be careful with class variables that are mutable.

class BadExample:
    shared_list = []  # All instances share this list!

a = BadExample()
b = BadExample()
a.shared_list.append("Oops")
print(b.shared_list)  # ['Oops']

Use instance variables (self.list = []) unless you explicitly want shared state.

Over-engineering: Not everything needs a class. Sometimes a function is simpler.

# Don't do this:
class StringReverser:
    def reverse(self, s):
        return s[::-1]

# Just do this:
def reverse_string(s):
    return s[::-1]

OOP in Python is a tool, not a religion. Use classes when they simplify your code. When they add complexity for no benefit, reach for functions and plain data structures instead.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.