Python OOP Explained Simply: Classes and Objects for Real Beginners
โก Quick Answer
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
Advertisement
Python OOP Explained Simply: Classes and Objects for Real Beginners
Object-Oriented Programming (OOP) is a way of structuring code around "objects" โ bundles of data and the functions that act on that data โ instead of separate variables and standalone functions. A class is the blueprint (like a cookie cutter); an object is one thing made from that blueprint (like an individual cookie).
Most Python beginners hit a wall here. Variables, loops, and functions feel fine for the first two weeks โ then classes show up and nothing clicks. The usual culprit isn't that OOP is hard; it's that most tutorials lean on abstract examples (bank accounts, shapes) disconnected from anything you'd actually build.
This guide uses examples pulled from real programs instead.
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 listThe 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 โ unchangedInheritance โ 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: Rexsuper() 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)) # 5A 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
The verdict: reach for OOP when data and behavior naturally travel together and you need multiple instances; stick with plain functions for one-off scripts and simple transformations.
| Situation | Better fit |
|---|---|
Data and behavior belong together (e.g., a User with .deactivate()) | OOP |
| You'll create many instances of the same concept | OOP |
| Codebase is complex enough to need organization/reuse | OOP |
| Quick script or CLI utility | Plain functions |
| One-off data transformation (e.g., cleaning a CSV) | Plain functions |
| Class design overhead outweighs the benefit | Plain functions |
Most production Python โ Django models, FastAPI schemas, ORMs โ leans on OOP because it keeps related state and logic in one place. Small scripts rarely need it.
Further Reading
- Python for Absolute Beginners: Your First 30 Days Roadmap (2025)
- The Python Libraries Every Developer Must Know in 2025
- Python File Handling Guide: Read, Write, Process Any File Type
- Python Pandas Tutorial 2026 โ Data Analysis for Beginners to Pro
- Python Testing with Pytest: Write Tests That Actually Catch Bugs
- React vs Next.js vs Remix: Which Framework Should You Choose?
- How to Use GitHub Copilot to 5x Your Coding Speed
- Terminal and Command Line Mastery: 30 Commands That Changed My Life
Advertisement
๐ฌ DiscussionPowered by GitHub Discussions
Frequently Asked Questions
Problem Solver and Cloud Expert
Solves complex infrastructure challenges and architects reliable, scalable cloud deployments for every project. Shamshur Rahman keeps AiTechWorldsโ hosting and cloud infrastructure fast, resilient, and cost-efficient.
Not sure yet? Ask AI about this article
Get an instant, unbiased AI summary of โPython OOP Explained Simply: Classes and Objects for Real Beginnersโ.
Advertisement
Related Articles
The Python Libraries Every Developer Must Know in 2026
The essential Python libraries for 2026: 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 2026: Which Framework Should You Learn?
An honest Django vs Flask comparison for 2026 โ 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 2026: installation, essential shortcuts, best practices, and how data scientists use Jupyter for exploration, analysis, and sharing.