Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →

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.

A
AiTechWorlds Team
May 27, 2026 7 min read
📱

Get more content like this on Telegram!

Daily AI tips, notes & resources — free

Join 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 object
  • self refers to the specific object being created/used
  • laptop = 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.

Share this article:

Frequently Asked Questions

A class is a blueprint for creating objects. It defines what data (attributes) and behaviors (methods) objects of that type will have. Example: a `Dog` class defines that every dog has a name and breed (attributes) and can bark and learn tricks (methods). An object is a specific instance created from that blueprint: `my_dog = Dog('Rex', 'Labrador')` creates one specific dog. Think of a class as a cookie cutter and objects as the individual cookies.
A

AiTechWorlds Team

✓ Verified Writer

The 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

10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!