Object-Oriented Programming

Reviewed & published by Brayan K

Master classes, objects, and design principles to write structured, reusable, professional-grade code.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Introduction — Why Learn OOP

Think of OOP like building with LEGO. Instead of one giant pile of bricks (procedural code), you have organized sets with instructions (classes) that you can use to build specific things (objects). You can reuse the same instructions to build multiple houses, cars, or robots!

Up to now, you've written procedural code — a series of instructions that run top-to-bottom. That works for small scripts, but large projects quickly become messy.

Object-Oriented Programming (OOP) fixes that. It organizes code around objects — bundles of data (attributes) and behavior (methods) that represent real-world concepts.

ConceptWhat It MeansReal-World Example
ClassA blueprint/templateA cookie cutter shape
ObjectAn instance of a classAn actual cookie made from the cutter
AttributeData/properties of an objectCookie's flavor, size, color
MethodActions an object can doCookie can be eaten, decorated

This structure powers everything from games and AI frameworks to mobile apps and APIs built in Python.

2️⃣ What Are Classes and Objects?

A class defines the blueprint or concept (like "Dog"). An object (or instance) is a specific example of that concept (like "Buddy").

💡 How to Read This: The class Dog: line creates the blueprint. The dog1 = Dog("Buddy", 3) line creates an actual dog object named Buddy who is 3 years old.

class Dog:                          # Step 1: Define the blueprint
    def __init__(self, name, age):  # Step 2: Setup method (runs when creating)
        self.name = name            # Step 3: Store the name
        self.age  = age             # Step 4: Store the age

    def bark(self):                 # Step 5: Define a behavior
        print(f"{self.name} says Woof!")

# Creating objects (instances)
dog1 = Dog("Buddy", 3)              # Creates a Dog named Buddy, age 3
dog2 = Dog("Max", 5)                # Creates a Dog named Max, age 5

dog1.bark()                         # Buddy barks
dog2.bark()                         # Max barks
Buddy says Woof!  
Max says Woof!

Each object has its own data (name, age) but shares the same class definition.

# Object-Oriented Programming Practice

class Dog:
    def __init__(self, name, age, breed):
        self.name = name
        self.age = age
        self.breed = breed
    
    def bark(self):
        print(f"{self.name} says Woof!")
    
    def describe(self):
        print(f"{self.name} is a {self.age}-year-old {self.breed}")

# Create objects (instances)
dog1 = Dog("Buddy", 3, "Golden Retriever")
dog2 = Dog("Max", 5, "German Shepherd")

# Call methods
dog1.describe()
dog1.bark()

print()  # Empty line

dog2.describe()
dog2.bark()

# Access attributes
print(f"\n{dog1.name}'s breed: {dog1.breed}")

# Challenge: Create your own class for a Car or Person!

# ✅ Expected output:
# Buddy is a 3-year-old Golden Retriever
# Buddy says Woof!
#
# Max is a 5-year-old German Shepherd
# Max says Woof!
#
# Buddy's breed: Golden Retriever

3️⃣ Understanding the self Parameter

🔑 Key Concept: What is "self"?

self is like saying "me" or "this object". When a dog object calls bark(), self refers to THAT specific dog. It's how the object knows its own name, age, etc.

Inside a class, every method receives self as the first argument. self represents the current object, allowing access to its own data.

class Cat:
    def __init__(self, name):
        self.name = name       # self.name = "this cat's name"

    def meow(self):
        # self.name refers to THIS cat's name
        print(f"{self.name} says Meow!")

cat = Cat("Luna")
cat.meow()  # Luna says Meow!

💡 Remember: You MUST include self as the first parameter in every method inside a class. Python passes it automatically when you call the method — you never type it yourself!

When you call cat.meow(), Python automatically passes that instance as self.

4️⃣ Class Attributes vs Instance Attributes

Class Attributes

These are shared by all objects of that class.

class Dog:
    species = "Canis familiaris"   # class attribute

    def __init__(self, name):
        self.name = name           # instance attribute

dog1 = Dog("Buddy")
dog2 = Dog("Max")

print(Dog.species)  # Canis familiaris
print(dog1.species) # Canis familiaris

Change Dog.species once and it affects every dog.

Instance Attributes

Belong only to that object.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

Each dog keeps its own name and age.

5️⃣ Adding Behavior with Methods

Methods are functions inside classes that act on that object's data.

class Calculator:
    def add(self, a, b):
        return a + b

    def multiply(self, a, b):
        return a * b

calc = Calculator()
print(calc.add(5, 3))
print(calc.multiply(4, 6))
8  
24

Whenever you see a design that repeats logic, wrap it inside a method — it's cleaner and reusable.

6️⃣ Constructors — __init__()

__init__() is like the "birth certificate" of an object. When a baby is born, you record their name, birth date, etc. Similarly, when an object is created, __init__() sets up all its initial information.

The special __init__() method (called a constructor) runs automatically every time you create a new object.

PartMeaning
def __init__(self, ...):Define the constructor method
selfThe object being created
self.name = nameSave the parameter as an attribute
class Student:
    def __init__(self, name, grade):   # Runs automatically when creating
        self.name  = name              # Store name in the object
        self.grade = grade             # Store grade in the object
        print(f"Student {self.name} created.")

s1 = Student("Boopie", "A+")  # __init__ runs here automatically!
Student Boopie created.

You can think of __init__ as the setup stage for each object.

7️⃣ Magic (Special) Methods in Python

Python classes can override built-in behavior by defining dunder (double-underscore) methods.

MethodPurposeExample
__init__()ConstructorInitialize attributes
__str__()Human-friendly stringUsed by print(obj)
__repr__()Official representationUsed in debug tools
__len__()Length behaviorMakes len(obj) work
__add__()Add operatorobj1 + obj2
__eq__()Equality checkobj1 == obj2
class Dog:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return f"Dog named {self.name}"

dog = Dog("Buddy")
print(dog)
Dog named Buddy

8️⃣ Encapsulation — Protecting Data

Encapsulation is like a vending machine. You can see what's available and insert money (public interface), but you can't reach inside and grab items directly (private data). The machine controls how you interact with it.

Encapsulation means keeping data safe inside a class, exposing only what's needed. You hide details using private attributes (by convention, prefix with underscore).

ConventionMeaningExample
namePublic - accessible anywhereself.name
_nameProtected - "please don't touch"self._balance
__namePrivate - name mangling appliedself.__secret
class BankAccount:
    def __init__(self, owner, balance):
        self.owner   = owner
        self._balance = balance  # "private" - don't access directly!

    def deposit(self, amount):
        if amount > 0:           # Validation! Can't deposit negative
            self._balance += amount

    def get_balance(self):       # Safe way to check balance
        return self._balance

account = BankAccount("Brayan", 500)
account.deposit(200)
print(account.get_balance())     # ✅ Use the method
# print(account._balance)        # ❌ Works but bad practice!

This approach is standard in finance, games, and apps where data integrity matters.

9️⃣ Abstraction — Simplify Complexity

Abstraction hides unnecessary details so the user focuses on what matters.

Example: You use print() without worrying about how text is sent to stdout.

You can design your own abstractions:

class CoffeeMachine:
    def brew(self):
        print("Heating water…")
        print("Grinding beans…")
        print("Pouring coffee…")
        print("Coffee ready!")

All the complex steps stay hidden inside brew().

🔟 Inheritance — Reuse Existing Code

Inheritance is like family traits. A child inherits features from parents (eye color, height), but can also have their own unique features. In code, a "child" class inherits from a "parent" class.

(Full lesson follows next module, but short preview here.)

TermAlso CalledDescription
Parent ClassBase / Super classThe class being inherited FROM
Child ClassDerived / Sub classThe class that inherits

Inheritance lets one class extend another:

class Animal:                 # Parent class
    def speak(self):
        print("Some sound")

class Dog(Animal):            # Child class - inherits from Animal
    def speak(self):          # Override parent's method
        print("Woof!")

# Dog gets everything from Animal, plus its own version of speak()
dog = Dog()
dog.speak()  # Output: Woof!

Now Dog inherits everything from Animal and customizes speak().

11️⃣ Polymorphism — Same Name, Different Behavior

Polymorphism means "many forms". Think of the word "open" — you can open a door, open a book, open an app. Same word, different actions depending on what you're opening. In code, different objects respond to the same method call in their own way.

Different objects can share method names but act differently:

class Bird:
    def make_sound(self): print("Chirp")

class Cat:
    def make_sound(self): print("Meow")

class Dog:
    def make_sound(self): print("Woof")

# All have make_sound() but each does something different!
animals = [Bird(), Cat(), Dog()]
for animal in animals:
    animal.make_sound()  # Python figures out which version to call
Chirp  
Meow
Woof

💡 Why This Matters: You can write code that works with ANY object that has a certain method, without knowing the exact type. This makes your code flexible and extensible!

12️⃣ Designing Readable Classes

Good Naming

Use nouns for class names (Book, Student) and verbs for methods (calculate_total, display_info).

Docstrings

Document each class and method for clarity.

class Rectangle:
    """Represents a 2D rectangle with width and height."""
    def __init__(self, w, h):
        self.width = w
        self.height = h

13️⃣ Class vs Static Methods

Class methods operate on the class itself, not instances. Static methods don't use self or cls — they're utility functions inside a class.

TypeFirst ParameterUse Case
Instance methodselfNeeds object data
@classmethodclsNeeds class data, factory methods
@staticmethodnoneUtility function, no class/object data needed
class MathTools:
    @staticmethod
    def add(a, b):              # No self or cls needed
        return a + b

    @classmethod
    def identity(cls):          # cls = the class itself
        return f"This is class {cls.__name__}"

# Call without creating an object!
print(MathTools.add(3, 5))      # Output: 8
print(MathTools.identity())     # Output: This is class MathTools

14️⃣ Practical Example — Book Class 📚

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

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

book = Book("1984", "George Orwell", 328)
print(book)
'1984' by George Orwell (328 pages)

15️⃣ Practical Example — Rectangle Class 📏

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

rect = Rectangle(5, 10)
print("Area:", rect.area())
print("Perimeter:", rect.perimeter())

16️⃣ Practical Example — Student Class 🎓

class Student:
    def __init__(self, name):
        self.name = name
        self.grades = []

    def add_grade(self, grade):
        self.grades.append(grade)

    def average(self):
        return sum(self.grades) / len(self.grades)

s = Student("Boopie")
s.add_grade(95)
s.add_grade(88)
s.add_grade(92)
print("Average:", s.average())

This model mirrors real-life objects — names, attributes, behaviors.

17️⃣ Practical Example — ShoppingCart 🛒

class ShoppingCart:
    def __init__(self):
        self.items = {}

    def add_item(self, name, price, quantity=1):
        self.items[name] = self.items.get(name, 0) + price * quantity

    def remove_item(self, name):
        if name in self.items: del self.items[name]

    def total(self):
        return sum(self.items.values())

cart = ShoppingCart()
cart.add_item("Apple", 1.5, 4)
cart.add_item("Banana", 0.8, 6)
print("Total:", cart.total())

18️⃣ Advanced OOP Concepts — Deep Dive

Inheritance in Detail

Reuse and extend parent behavior.

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "Sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

dog = Dog("Buddy")
print(dog.speak())

Multiple Inheritance

Python allows combining behaviors:

class Flyer:
    def fly(self): print("Flying")

class Swimmer:
    def swim(self): print("Swimming")

class Duck(Flyer, Swimmer):
    pass

d = Duck()
d.fly()
d.swim()

Polymorphism in Practice

Different objects responding to the same message:

animals = [Dog("Max"), Animal("Creature")]
for a in animals:
    print(a.speak())

Composition — "Has a" Relationship

Instead of inheriting, you can combine objects.

class Engine:
    def start(self): print("Engine started.")

class Car:
    def __init__(self):
        self.engine = Engine()
    def drive(self):
        self.engine.start()
        print("Car moving.")

car = Car()
car.drive()

19️⃣ Practical Mini-Project — Library System 📖

Bringing it all together:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author
        self.borrowed = False

class Library:
    def __init__(self):
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def borrow_book(self, title):
        for b in self.books:
            if b.title == title and not b.borrowed:
                b.borrowed = True
                print(f"You borrowed '{title}'.")
                return
        print("Book unavailable.")

library = Library()
library.add_book(Book("1984","Orwell"))
library.add_book(Book("Dune","Herbert"))
library.borrow_book("Dune")

This project demonstrates encapsulation, composition, and real-world thinking in code design.

# Comprehensive OOP Example
class Person:
    # Class attribute
    species = "Homo sapiens"
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        print(f"Hi, I'm {self.name} and I'm {self.age} years old")
    
    def birthday(self):
        self.age += 1
        print(f"Happy birthday! Now {self.age} years old")
    
    def __str__(self):
        return f"Person({self.name}, {self.age})"

# Create objects
person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

person1.introduce()
person2.introduce()

# Bank Account Example
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited {amount}. New balance: {self.balance}")
    
    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient funds!")
        else:
            self.balance -= amount
            print(f"Withdrew {amount}. New balance: {self.balance}")

# Test bank account
print("\n--- Bank Account ---")
account = BankAccount("Alice", 100)
account.deposit(50)
account.withdraw(30)
account.withdraw(200)

# ✅ Expected output:
# Hi, I'm Alice and I'm 25 years old
# Hi, I'm Bob and I'm 30 years old
#
# --- Bank Account ---
# Deposited 50. New balance: 150
# Withdrew 30. New balance: 120
# Insufficient funds!

20️⃣ When to Use OOP

If your code is short and one-off, functions may be enough. But for any project you plan to grow, OOP keeps it organized and maintainable.

21️⃣ Best Practices ✅

These habits match professional guidelines (PEP 8 and SOLID principles).

22️⃣ Common Errors and Fixes 🧯

ErrorCauseSolution
TypeError: missing 1 required positional argumentForgot self in method definitionAdd self as first parameter
AttributeErrorAccessing nonexistent attributeCheck name and constructor
NameErrorUsed variable before definingInitialize inside __init__()
Circular importTwo modules import each otherRestructure code into packages

Remember that Python module names are case-sensitive on most systems.

🎯 23️⃣ Mini Challenges

Practice these to solidify every OOP concept you've learned.

24️⃣ Expert Insights (For Future Learning)

25️⃣ Summary 🧾

OOP turns code into living models of real systems.

With OOP mastered, you're ready to explore Inheritance and Polymorphism in depth in the next lesson.

📋 Quick Reference — OOP

class Dog:Define a class
def __init__(self, name):Constructor / initialiser
self.name = nameSet instance variable
Dog("Rex")Create an object
isinstance(d, Dog)Check object type

🎉 Great work! You've completed this lesson.

You now understand classes, objects, attributes, methods, and the four OOP pillars. These concepts power virtually every Python framework and library.

Practice quiz

What is the relationship between a class and an object?

  • An object is a blueprint; a class is a specific instance
  • They are exactly the same thing
  • A class is a blueprint/template; an object is a specific instance of it
  • A class can only ever create one object

Answer: A class is a blueprint/template; an object is a specific instance of it. The class defines the blueprint (like Dog) and an object is a concrete instance (like Buddy).

Which special method runs automatically when you create a new object?

  • __init__
  • __str__
  • __new__only
  • __call__

Answer: __init__. __init__ is the constructor; it runs automatically to set up each new instance.

What does the first parameter, self, in an instance method represent?

  • The class itself
  • A required keyword argument you must pass manually
  • The module the class lives in
  • The current instance the method is called on

Answer: The current instance the method is called on. self refers to the specific object; Python passes it automatically when you call obj.method().

What is the difference between a class attribute and an instance attribute?

  • Class attributes are private; instance attributes are public
  • A class attribute is shared by all instances; an instance attribute belongs to one object
  • There is no difference
  • Instance attributes are shared; class attributes are per-object

Answer: A class attribute is shared by all instances; an instance attribute belongs to one object. Class attributes (e.g. species) are shared across instances; instance attributes (e.g. self.name) are per object.

Which dunder method is used by print(obj) to produce a human-friendly string?

  • __str__
  • __repr__
  • __len__
  • __init__

Answer: __str__. print() uses __str__ for a readable representation; __repr__ is the official/debug representation.

By Python convention, what does a single leading underscore (self._balance) signal?

  • The attribute is strictly enforced as private
  • It triggers name mangling
  • It is 'protected' — a hint that it shouldn't be accessed directly
  • It is a class attribute

Answer: It is 'protected' — a hint that it shouldn't be accessed directly. A single underscore is a convention meaning 'internal, please don't touch'; it is not strictly enforced.

In 'class Dog(Animal):', what does Dog overriding speak() achieve?

  • It deletes Animal.speak permanently
  • Dog inherits Animal's members but provides its own version of speak()
  • It prevents Dog from being instantiated
  • It calls Animal.speak twice

Answer: Dog inherits Animal's members but provides its own version of speak(). Dog inherits from Animal and overrides speak() with its own implementation.

Polymorphism in OOP means:

  • A class can have only one method
  • Objects cannot share method names
  • Methods must all return the same type
  • Different objects can respond to the same method name in their own way

Answer: Different objects can respond to the same method name in their own way. Polymorphism lets different types implement the same method (e.g. make_sound()) with different behaviour.

Which decorator marks a method that takes NO self or cls and uses no class/object data?

  • @classmethod
  • @staticmethod
  • @property
  • @instancemethod

Answer: @staticmethod. @staticmethod defines a utility function inside a class that receives neither self nor cls.

What does composition (a "has a" relationship) look like, e.g. Car and Engine?

  • class Car(Engine): pass
  • Engine inherits from Car
  • A Car holds an Engine object as an attribute (self.engine = Engine())
  • They must be the same class

Answer: A Car holds an Engine object as an attribute (self.engine = Engine()). Composition means a Car contains an Engine instance rather than inheriting from it.

Continue this course