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
Advertisement
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.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.