Python OOP Explained Simply: Classes and Objects for Real Beginners
Python OOP explained simply: classes, objects, inheritance, and encapsulation with real examples that make object-oriented programming click for beginners.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Python OOP Explained Simply: Classes and Objects for Real Beginners
OOP (Object-Oriented Programming) is where most Python beginners hit a wall. The first two weeks feel fine — variables, loops, functions. Then classes appear and suddenly nothing makes sense.
I hit that wall too. Looking back, the issue wasn't that OOP is hard — it's that most explanations use bad examples (bank accounts, shapes) that feel abstract and disconnected from real use cases.
This guide uses examples from programs you'd actually build.
The Problem OOP Solves
Before showing what OOP is, let me show why you'd want it.
Imagine tracking products in a store:
# Without OOP — messy and hard to scale
product1_name = "Laptop"
product1_price = 999.99
product1_stock = 10
product2_name = "Mouse"
product2_price = 29.99
product2_stock = 50
def apply_discount(price, discount_pct):
return price * (1 - discount_pct)
print(apply_discount(product1_price, 0.10))
What if you have 100 products? This approach breaks down. Variables get unwieldy. Functions need to receive individual pieces of data.
OOP solves this by bundling related data and functions together.
Your First Class
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def apply_discount(self, discount_pct):
self.price = self.price * (1 - discount_pct)
def __str__(self):
return f"{self.name}: ${self.price:.2f} ({self.stock} in stock)"
# Create products (objects/instances)
laptop = Product("Laptop", 999.99, 10)
mouse = Product("Mouse", 29.99, 50)
# Use them
laptop.apply_discount(0.10)
print(laptop) # Laptop: $899.99 (10 in stock)
print(mouse) # Mouse: $29.99 (50 in stock)
What just happened:
class Product:defines the blueprint__init__is the constructor — runs when you create an objectselfrefers to the specific object being created/usedlaptop = Product(...)creates one specific product
Understanding __init__ and self
class Dog:
def __init__(self, name, breed, age):
# self.name = the Dog's name attribute
# name = the argument passed in
self.name = name # Store name on this specific dog
self.breed = breed
self.age = age
self.tricks = [] # Every new dog starts with no tricks
def learn_trick(self, trick):
self.tricks.append(trick)
print(f"{self.name} learned {trick}!")
def bark(self):
print(f"{self.name} says: Woof!")
def describe(self):
return f"{self.name} is a {self.age}-year-old {self.breed}"
rex = Dog("Rex", "Labrador", 3)
buddy = Dog("Buddy", "Beagle", 5)
rex.bark() # Rex says: Woof!
rex.learn_trick("sit") # Rex learned sit!
rex.learn_trick("shake") # Rex learned shake!
print(rex.describe()) # Rex is a 3-year-old Labrador
print(rex.tricks) # ['sit', 'shake']
print(buddy.tricks) # [] — Buddy has his own list
The key insight: each object has its own copy of the attributes. Changing rex.tricks doesn't affect buddy.tricks.
Class Attributes vs Instance Attributes
class Employee:
company = "AiTechWorlds" # Class attribute — shared by all employees
employee_count = 0
def __init__(self, name, role, salary):
self.name = name # Instance attribute — unique to each employee
self.role = role
self.salary = salary
Employee.employee_count += 1 # Increment shared counter
def get_raise(self, amount):
self.salary += amount
emp1 = Employee("Alice", "Engineer", 90000)
emp2 = Employee("Bob", "Designer", 80000)
print(emp1.company) # AiTechWorlds
print(emp2.company) # AiTechWorlds
print(Employee.employee_count) # 2
emp1.get_raise(5000)
print(emp1.salary) # 95000
print(emp2.salary) # 80000 — unchanged
Inheritance — Building on Existing Classes
Inheritance lets a new class start with all the attributes and methods of an existing class, then add or override things.
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"Animal: {self.name}"
class Dog(Animal): # Dog inherits from Animal
def __init__(self, name):
super().__init__(name, "Woof") # Call parent's __init__
self.tricks = []
def learn_trick(self, trick):
self.tricks.append(trick)
def speak(self): # Override parent's method
return f"{self.name} barks: {self.sound}!"
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, "Meow")
self.indoor = indoor
def speak(self):
return f"{self.name} purrs and says {self.sound}"
dog = Dog("Rex")
cat = Cat("Whiskers")
print(dog.speak()) # Rex barks: Woof!
print(cat.speak()) # Whiskers purrs and says Meow
# Dog still has Animal's __str__
print(str(dog)) # Animal: Rex
super() calls the parent class method. It lets you extend instead of replace parent behavior.
Encapsulation — Protecting Data
class BankAccount:
def __init__(self, owner, initial_balance):
self.owner = owner
self._balance = initial_balance # _ means "private by convention"
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
return self._balance
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
return self._balance
@property
def balance(self): # Property — read-only access
return self._balance
def __str__(self):
return f"Account({self.owner}: ${self._balance:.2f})"
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account.balance) # 1300
print(account) # Account(Alice: $1300.00)
# account._balance = -999 # You CAN do this — Python doesn't truly enforce
# But the _ prefix signals "don't access this directly"
The @property decorator creates a read-only attribute. account.balance runs the balance method but looks like attribute access.
Dunder Methods (Magic Methods)
Python uses __dunder__ methods to define how objects behave with built-in operations:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __add__(self, other): # p1 + p2
return Point(self.x + other.x, self.y + other.y)
def __eq__(self, other): # p1 == p2
return self.x == other.x and self.y == other.y
def __len__(self): # len(p)
return int((self.x**2 + self.y**2) ** 0.5)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1 + p2) # Point(4, 6)
print(p1 == p2) # False
print(len(p2)) # 5
A Real-World OOP Example
Here's OOP applied to a web API context — the kind of structure you'd see in a FastAPI app:
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class User:
id: int
email: str
name: str
created_at: datetime = field(default_factory=datetime.now)
is_active: bool = True
def deactivate(self):
self.is_active = False
def __str__(self):
return f"User({self.name}, {self.email})"
class UserRepository:
def __init__(self):
self._users: dict[int, User] = {}
self._next_id = 1
def create(self, email: str, name: str) -> User:
user = User(id=self._next_id, email=email, name=name)
self._users[self._next_id] = user
self._next_id += 1
return user
def get(self, user_id: int) -> Optional[User]:
return self._users.get(user_id)
def get_all(self) -> list[User]:
return list(self._users.values())
def delete(self, user_id: int) -> bool:
if user_id in self._users:
del self._users[user_id]
return True
return False
# Usage
repo = UserRepository()
alice = repo.create("alice@example.com", "Alice")
bob = repo.create("bob@example.com", "Bob")
print(repo.get_all())
alice.deactivate()
print(f"Alice active: {alice.is_active}")
This pattern — a model class + a repository class — is used in almost every production Python application. See our FastAPI tutorial to see how this structure integrates with a real API.
When to Use OOP vs Functions
Use OOP when:
- You have data and behaviors that naturally belong together
- You'll create multiple instances of the same concept
- Your code is complex enough to benefit from organization
Use plain functions when:
- Simple scripts and utilities
- One-off data transformations
- The overhead of class design isn't worth it
Frequently Asked Questions
What is a class in Python?
A blueprint for creating objects. Defines what data (attributes) and behaviors (methods) objects will have.
Why use OOP?
Organizes related data and functions, enables reuse through inheritance, and makes large programs manageable.
What is self?
A reference to the specific object instance. Python passes it automatically when calling instance methods.
Class vs object difference?
Class = blueprint. Object = specific thing built from the blueprint.
Final Thoughts
OOP clicks when you stop thinking of it as a programming technique and start thinking of it as a way to model the real world in code. Every entity in your program (User, Product, Request, Task) can be a class with relevant data and behaviors.
The learning path: start with simple classes (1–2 attributes, 2–3 methods). Add inheritance when you find yourself copying code between classes. Use dunder methods when you want objects to work naturally with Python operators.
For applying OOP in automation scripts, see our Python automation scripts guide which shows class-based automation patterns. For the Python libraries that use OOP extensively (SQLAlchemy models, Pydantic models), our best Python libraries guide covers the practical applications.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
The Python Libraries Every Developer Must Know in 2025
The essential Python libraries for 2025: from requests and pandas to FastAPI and LangChain — what each does, when to use it, and how to get started quickly.
Django vs Flask in 2025: Which Framework Should You Learn?
An honest Django vs Flask comparison for 2025 — which Python framework to learn first, when each excels, and why FastAPI has changed the equation.
FastAPI Tutorial: Building Your First REST API in 30 Minutes
A hands-on FastAPI tutorial for beginners: build a fully functional REST API in 30 minutes with CRUD endpoints, request validation, and automatic docs.
Jupyter Notebook Guide: The Data Scientist's Favorite Tool
A complete Jupyter Notebook guide for 2025: installation, essential shortcuts, best practices, and how data scientists use Jupyter for exploration, analysis, and sharing.