Introduction to Object-Oriented Programming (OOP) in Python

SA
StudyAI
AI-generated study notes
· Published Updated Pro Plus model

From the Classes(python) curriculum

TL;DR

Object-Oriented Programming (OOP) is a powerful way to organize your code by modeling real-world entities as "objects" that combine data and behavior. It helps you write more organized, reusable, and maintainable Python programs. You'll learn core concepts like classes, objects, attributes, and methods to build structured applications.

1. The Mental Model

Think of OOP as building with LEGOs instead of just loose bricks. You're creating self-contained units (the LEGO pieces) that have specific characteristics and actions. When you program with OOP, you define blueprints for these units, then you can easily create many copies of them to solve your problem. This makes your code much easier to understand and manage, especially as projects grow.

2. The Core Material

Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around "objects" rather than "functions and logic." In Python, everything is an object, but OOP allows us to create our own custom objects.

2.1 Classes and Objects: The Blueprint and the House

Top view of a laptop and architectural blueprint on white marble surface, showcasing modern workspace.
Photo by Mikhail Nilov on Pexels

At the heart of OOP are classes and objects.
* A class is like a blueprint or a cookie cutter. It defines the structure and behavior that its instances will have. It doesn't do anything on its own; it just describes what something can be.
* An object (or instance) is a concrete realization of a class. It's like the house built from the blueprint, or a specific cookie made from the cutter. You can create many objects from a single class.

Let's look at a simple Dog class:

class Dog:
    # Class attribute - shared by all instances of Dog
    species = "Canis familiaris"

    def __init__(self, name, breed):
        """
        The constructor method.
        It initializes a new Dog object when it's created.
        'self' refers to the instance of the class.
        """
        self.name = name  # Instance attribute
        self.breed = breed  # Instance attribute

    def bark(self):
        """
        An instance method.
        It defines an action that a Dog object can perform.
        """
        return f"{self.name} says Woof!"

# Creating objects (instances) from the Dog class
my_dog = Dog("Buddy", "Golden Retriever")
your_dog = Dog("Lucy", "Beagle")

# Accessing attributes
print(f"My dog's name is {my_dog.name} and she is a {my_dog.breed}.")
print(f"Your dog's name is {your_dog.name} and he is a {your_dog.breed}.")

# Accessing a class attribute
print(f"All dogs belong to the species: {Dog.species}")

# Calling methods
print(my_dog.bark())
print(your_dog.bark())

In this example:
* Dog is the class.
* my_dog and your_dog are objects (instances) of the Dog class.
* species is a class attribute, shared by all Dog objects.
* name and breed are instance attributes, unique to each Dog object.
* __init__ is a special method called the constructor. It's automatically called when you create a new object. self is a reference to the current instance of the class.
* bark is an instance method, defining an action Dog objects can perform.

2.2 Encapsulation: Bundling Data and Behavior

Close-up of a blue screen error shown on a data center control terminal.
Photo by panumas nikhomkhai on Pexels

Encapsulation is about bundling the data (attributes) and methods (behaviors) that operate on the data into a single unit, which is the object. It also means restricting direct access to some of an object's components, preventing unintended modification. This is often achieved through conventions in Python (using a single or double underscore prefix for "private" attributes) rather than strict access modifiers like private in other languages.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance  # Using __ to hint that it's "private"

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"Deposited {amount}. New balance: {self.__balance}")
        else:
            print("Deposit amount must be positive.")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"Withdrew {amount}. New balance: {self.__balance}")
        else:
            print("Invalid withdrawal amount or insufficient funds.")

    def get_balance(self):
        return self.__balance

# Create an account
account = BankAccount("Alice")
print(f"Initial balance: {account.get_balance()}")

account.deposit(100)
account.withdraw(30)
account.withdraw(200) # This will fail
print(f"Final balance: {account.get_balance()}")

# Trying to access __balance directly (discouraged)
# print(account.__balance) # This would raise an AttributeError if not for name mangling
print(account._BankAccount__balance) # This works, but it's an anti-pattern

Here, __balance is "encapsulated." We provide deposit, withdraw, and get_balance methods to interact with it, ensuring that the balance is always handled correctly. The __ prefix triggers Python's name mangling, making direct access slightly harder (e.g., _BankAccount__balance), but it doesn't truly prevent it like private in Java or C++. It's a strong convention to indicate "don't touch this directly."

2.3 Inheritance: Building on Existing Code

Close-up of HTML code lines highlighting web development concepts and techniques.
Photo by Pixabay on Pexels

Inheritance allows a new class (the child or derived class) to inherit attributes and methods from an existing class (the parent or base class). This promotes code reuse and establishes a "is-a" relationship (e.g., a Cat is a Animal).

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method")

    def eat(self):
        return f"{self.name} is eating."

class Dog(Animal): # Dog inherits from Animal
    def __init__(self, name, breed):
        super().__init__(name) # Call the parent's constructor
        self.breed = breed

    def speak(self): # Overriding the speak method
        return f"{self.name} says Woof!"

class Cat(Animal): # Cat inherits from Animal
    def __init__(self, name, color):
        super().__init__(name)
        self.color = color

    def speak(self): # Overriding the speak method
        return f"{self.name} says Meow!"

# Create objects
doggo = Dog("Rex", "German Shepherd")
kitty = Cat("Whiskers", "Orange")

print(doggo.speak())
print(doggo.eat()) # Inherited method
print(kitty.speak())
print(kitty.eat()) # Inherited method

Here, Dog and Cat are child classes of Animal. They inherit name and eat() from Animal and also define their own speak() method, overriding the parent's generic one. super().__init__(name) is crucial for calling the parent class's constructor to initialize inherited attributes.

2.4 Polymorphism: "Many Forms"

Close-up of HTML and PHP code on screen showing error message and login form data.
Photo by Markus Spiske on Pexels

Polymorphism means "many forms." In OOP, it allows objects of different classes to be treated as objects of a common type. It often works hand-in-hand with inheritance. If Dog and Cat both inherit from Animal and have a speak() method, we can call speak() on either a Dog or Cat object without needing to know its exact type, and it will do the "right" thing.

def make_animal_speak(animal):
    print(animal.speak())

make_animal_speak(doggo) # Rex says Woof!
make_animal_speak(kitty) # Whiskers says Meow!

# Another example with a list of animals
animals = [Dog("Fido", "Poodle"), Cat("Garfield", "Tabby")]

for animal in animals:
    print(animal.name)
    print(animal.speak())

The make_animal_speak function works with any object that has a speak() method, demonstrating polymorphism. Each object speak()s in its own specific way.

Here's a visual summary of these core concepts:

graph TD
    A[Class] --> B[Object (Instance)]
    B -- has --> C[Attributes]
    B -- performs --> D[Methods]

    subgraph Encapsulation
        E[Object] -- bundles --> C
        E -- bundles --> D
        E -- controls access to --> C
    end

    F[Parent Class] -- "is-a" relationship --> G[Child Class]
    G -- inherits --> H[Attributes & Methods] from F
    G -- can override --> H
    F -- "many forms" --> I[Polymorphism]

    J[Different Objects] -- treated uniformly via --> I
    J -- e.g., calling same method on --> G

3. Worked Example

Let's build a simple Library system using OOP principles. We'll have Book objects and a Library object to manage them.

class Book:
    def __init__(self, title, author, isbn):
        self.title = title
        self.author = author
        self.isbn = isbn
        self.is_checked_out = False # Initial state

    def __str__(self):
        """String representation of the Book object."""
        status = "Checked Out" if self.is_checked_out else "Available"
        return f'"{self.title}" by {self.author} (ISBN: {self.isbn}) - Status: {status}'

    def check_out(self):
        if not self.is_checked_out:
            self.is_checked_out = True
            return f'"{self.title}" has been checked out.'
        else:
            return f'"{self.title}" is already checked out.'

    def return_book(self):
        if self.is_checked_out:
            self.is_checked_out = False
            return f'"{self.title}" has been returned.'
        else:
            return f'"{self.title}" was not checked out.'

class Library:
    def __init__(self, name):
        self.name = name
        self.books = [] # A list to store Book objects

    def add_book(self, book):
        if isinstance(book, Book): # Ensure we're adding a Book object
            self.books.append(book)
            print(f'Added "{book.title}" to the {self.name} library.')
        else:
            print("Can only add Book objects to the library.")

    def list_all_books(self):
        if not self.books:
            print(f"The {self.name} library currently has no books.")
            return

        print(f"\n--- Books in {self.name} ---")
        for book in self.books:
            print(book) # Uses the __str__ method of the Book object
        print("--------------------------")

    def find_book(self, search_term, by="title"):
        found_books = []
        for book in self.books:
            if by == "title" and search_term.lower() in book.title.lower():
                found_books.append(book)
            elif by == "author" and search_term.lower() in book.author.lower():
                found_books.append(book)
            elif by == "isbn" and search_term == book.isbn:
                found_books.append(book)
        return found_books

# --- Let's use our Library system ---
my_library = Library("City Central Library")

# Create some book objects
book1 = Book("The Hitchhiker's Guide to the Galaxy", "Douglas Adams", "978-0345391803")
book2 = Book("Pride and Prejudice", "Jane Austen", "978-0141439518")
book3 = Book("Python Crash Course", "Eric Matthes", "978-1593279288")

# Add books to the library
my_library.add_book(book1)
my_library.add_book(book2)
my_library.add_book(book3)

# List all books
my_library.list_all_books()

# Perform some actions
print(book1.check_out())
my_library.list_all_books() # See the status change

print(book1.check_out()) # Try checking out again
print(book2.check_out())
my_library.list_all_books()

print(book1.return_book())
my_library.list_all_books()

# Find books
print("\n--- Search Results ---")
found = my_library.find_book("python", by="title")
if found:
    for b in found:
        print(f"Found: {b}")
else:
    print("No books found matching 'python'.")

found_austin = my_library.find_book("Austen", by="author")
if found_austin:
    for b in found_austin:
        print(f"Found by author: {b}")

In this example:
* The Book class encapsulates book data (title, author, isbn, is_checked_out) and behaviors (check_out, return_book).
* The Library class manages a collection of Book objects. It has its own data (name, books list) and behaviors (add_book, list_all_books, find_book).
* The __str__ method in Book is a special method that defines how an object should be represented as a string, making print(book) output readable.

4. Production Pitfalls & Best Practices

4.1 Real-World Best Practices

  1. Use __init__ for Initial State: Always initialize an object's state (its attributes) in the __init__ method. This makes sure every new object starts in a predictable and valid condition.
  2. Favor Composition Over Inheritance: While inheritance is useful, don't overuse it. Often, creating objects that contain other objects (composition) is more flexible and maintains simpler hierarchies than deep inheritance chains. Think "has-a" relationships (Car has an Engine) rather than "is-a" (Engine is a Car).
  3. Encapsulate State with Properties (Getters/Setters): For more controlled access to attributes, especially when validation or side effects are needed, use Python's @property decorator to create "getters" and "setters" without sacrificing readability.
  4. Define __str__ and __repr__: Implement __str__ for a human-readable string representation (like our Book example) and __repr__ for an unambiguous, developer-focused representation (useful for debugging, often showing how to recreate the object).
  5. Small Classes, Single Responsibility: Aim for classes that are small and have a single, well-defined responsibility. This makes them easier to understand, test, and maintain.

4.2 Common Bugs & Anti-Patterns

  1. Modifying Class Attributes Accidentally:
    Sometimes, new Pythonistas mistakenly use class attributes where instance attributes are needed, leading to unintended side effects across all instances.

    BAD Code:
    ```python
    class BadExample:
    data_list = [] # This is a class attribute!

    def add_item(self, item):
        self.data_list.append(item)
    

    obj1 = BadExample()
    obj2 = BadExample()

    obj1.add_item("item A")
    obj2.add_item("item B")

    print(obj1.data_list) # Output: ['item A', 'item B'] - Uh oh!
    print(obj2.data_list) # Output: ['item A', 'item B'] - Both modified the same list!
    ```

    FIXED Code:
    ```python
    class GoodExample:
    def init(self):
    self.data_list = [] # Correct: instance attribute

    def add_item(self, item):
        self.data_list.append(item)
    

    obj1 = GoodExample()
    obj2 = GoodExample()

    obj1.add_item("item A")
    obj2.add_item("item B")

    print(obj1.data_list) # Output: ['item A']
    print(obj2.data_list) # Output: ['item B'] - Correct, each has its own list.
    `` 2. **Forgettingsuper().initin Inheritance:** When a child class has its owninitmethod, it must explicitly call the parent'sinit` if it wants to inherit the parent's initialized attributes.

    BAD Code:
    ```python
    class Parent:
    def init(self, value):
    self.value = value

    class Child(Parent):
    def init(self, value, extra): # Forgets to call parent's init
    self.extra = extra # self.value won't be set!

    child_obj = Child(10, "foo")

    print(child_obj.value) # AttributeError: 'Child' object has no attribute 'value'

    ```

    FIXED Code:
    ```python
    class Parent:
    def init(self, value):
    self.value = value

    class Child(Parent):
    def init(self, value, extra):
    super().init(value) # Correct: calls Parent's init
    self.extra = extra

    child_obj = Child(10, "foo")
    print(child_obj.value) # Output: 10
    print(child_obj.extra) # Output: foo
    ```

4.3 When to Use What (Decision Matrix)

Situation / Goal OOP Approach to Consider Why
Modeling real-world entities Classes and Objects Provides a natural way to represent entities with data and behavior.
Reusing common functionality Inheritance Allows child classes to inherit and extend parent class features.
Hiding internal details Encapsulation (e.g., private attributes with __) Protects data integrity and simplifies external interaction.
Handling different types uniformly Polymorphism Enables writing flexible code that works with various object types.
Bundling data and operations Methods on objects Keeps related data and behavior together, improving organization.
Managing a collection of items A class that contains other objects (Composition) Cleaner than deep inheritance; represents "has-a" relationships.

5. Now Try It

You've seen how Book and Library work. Your task is to extend this system.
Create a new class called Member. A Member should have a name, a member_id, and a list of borrowed_books.
Add a method to the Member class to borrow_book(book) and return_book(book). When a member borrows or returns a book, it should update both the Member's borrowed_books list AND the Book object's is_checked_out status. Make sure a member can't borrow a book already checked out and can't return a book they haven't borrowed.

Success looks like: You can create a Member, have them borrow a Book (which updates the book's status), and then return it, confirming all statuses update correctly.

Frequently asked about Introduction to Object-Oriented Programming (OOP) in Python

Object-Oriented Programming (OOP) is a powerful way to organize your code by modeling real-world entities as "objects" that combine data and behavior. It helps you write more organized, reusable, and maintainable Python programs. Read the full notes above for the details.

Introduction to Object-Oriented Programming (OOP) in Python is a core topic in Classes(python). Most exam papers test it via a mix of definitions, worked examples, and applied problems. The notes above cover the high-yield sub-topics, common pitfalls, and the kind of questions examiners typically set.

Yes — every note in the StudyAI Campus Hub is free to read in full, right here on this page, with no account needed. If you clone the plan into your own dashboard, the free plan shows a preview of each note there; Basic and above unlock the full notes in your dashboard, along with practice quizzes, flashcards and offline study. You can always come back here to read the complete note for free.

Study this next


Get the full Classes(python) curriculum

Clone the complete plan to your dashboard for unlimited AI-generated notes, practice quizzes, and a personalised revision schedule.

Save this course free