Lesson 1 - Introduction to Object-Oriented Programming

Introduction

So far, we’ve been working with Python’s built-in data types like strings, lists, and dictionaries. While these are powerful, real-world applications often need custom data structures that better represent the things we’re modeling. This is where Object-Oriented Programming (OOP) comes in.

Object-oriented programming is a programming paradigm that organizes code around objects rather than just functions and logic. An object bundles together data (called attributes) and the functions that work with that data (called methods).

Think of it this way: when you’re building a bookstore application, you don’t just want a bunch of disconnected lists of book titles, prices, and authors. You want a Book object that keeps all that information together and knows how to do things like calculate discounts or display itself nicely.

In this lesson, we’ll learn:

  • What classes and objects are
  • How to create a class and instantiate objects
  • How to define attributes and methods
  • How to use the special __init__ method
  • What self means and why it’s important

What Are Classes and Objects?

A class is like a blueprint or template for creating objects. It defines what attributes (data) and methods (functions) the objects will have.

An object is an instance of a class—a specific example created from the blueprint.

For example:

  • Book could be a class (the blueprint)
  • A specific book like “Python Crash Course” would be an object (an instance of the Book class)

Let’s start with a simple example:

class Book:
    pass  # An empty class for now

We’ve created a class called Book. By convention, class names use PascalCase (capitalize each word). Now let’s create an object from this class:

my_book = Book()
print(my_book)

Output:

<__main__.Book object at 0x7f8b1c3e4a90>

We’ve created an object! The output shows it’s a Book object. Of course, this object doesn’t do much yet because our class is empty.

Adding Attributes

Let’s make our Book class more useful by adding attributes—the data that each book object will store. We’ll use the special __init__ method, which is automatically called when a new object is created.

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

Let’s break this down:

  • __init__ is a special method called a constructor. It runs automatically when you create a new object.
  • self refers to the object being created. Every method in a class takes self as its first parameter.
  • self.title = title takes the title argument and stores it as an attribute of the object.

Now we can create book objects with actual data:

book1 = Book("Python Crash Course", "Eric Matthes", 39.99)
book2 = Book("Automate the Boring Stuff", "Al Sweigart", 29.99)

print(book1.title)     # Python Crash Course
print(book1.author)    # Eric Matthes
print(book2.price)     # 29.99

Each object (book1 and book2) has its own set of attributes. Changing book1.price won’t affect book2.price.

Understanding self

The self parameter is crucial in OOP but can be confusing at first. Here’s what you need to know:

  • self represents the specific object (instance) that’s being worked with
  • When you call book1.title, Python automatically passes book1 as self
  • You don’t pass self yourself when calling methods—Python does it for you

Here’s an example to illustrate:

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

    def display_info(self):
        print(f"'{self.title}' by {self.author}")
        print(f"Price: ${self.price}")

book1 = Book("Python Crash Course", "Eric Matthes", 39.99)
book1.display_info()

Output:

'Python Crash Course' by Eric Matthes
Price: $39.99

When we call book1.display_info(), we don’t pass any arguments, but the method definition has self. Python automatically passes book1 as self, so inside the method, self.title refers to book1.title.

Adding Instance Methods

Methods are functions that belong to a class. They can access and modify the object’s attributes using self. Let’s add some useful methods to our Book class:

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

    def display_info(self):
        print(f"'{self.title}' by {self.author}")
        print(f"Price: ${self.price}")

    def apply_discount(self, discount_percent):
        discount_amount = self.price * (discount_percent / 100)
        self.price = self.price - discount_amount
        return self.price

    def is_expensive(self):
        return self.price > 30

# Create a book
book = Book("Learning Python", "Mark Lutz", 54.99)

# Use the methods
book.display_info()
print(f"Is expensive? {book.is_expensive()}")

new_price = book.apply_discount(20)
print(f"After 20% discount: ${new_price:.2f}")

Output:

'Learning Python' by Mark Lutz
Price: $54.99
Is expensive? True
After 20% discount: $43.99

Notice how methods can:

  • Take parameters (discount_percent)
  • Modify object attributes (self.price)
  • Return values (return self.price)
  • Access other attributes (self.price > 30)

A Practical Example: Managing a Bookstore

Let’s build a more complete example that manages a collection of books:

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

    def display_info(self):
        print(f"\n{self.title}")
        print(f"Author: {self.author}")
        print(f"Price: ${self.price}")
        print(f"In stock: {self.quantity}")

    def sell(self, amount=1):
        if amount > self.quantity:
            print(f"Not enough stock! Only {self.quantity} available.")
            return False
        else:
            self.quantity -= amount
            total = self.price * amount
            print(f"Sold {amount} book(s) for ${total:.2f}")
            return True

    def restock(self, amount):
        self.quantity += amount
        print(f"Restocked {amount} books. New quantity: {self.quantity}")

# Create our bookstore inventory
book1 = Book("Python Crash Course", "Eric Matthes", 39.99, 5)
book2 = Book("Automate the Boring Stuff", "Al Sweigart", 29.99, 3)
book3 = Book("Learning Python", "Mark Lutz", 54.99, 2)

# Display inventory
book1.display_info()
book2.display_info()
book3.display_info()

# Simulate sales
book1.sell(2)
book1.sell(4)  # This will fail - not enough stock
book1.restock(10)
book1.sell(4)  # Now it will work

Output:

Python Crash Course
Author: Eric Matthes
Price: $39.99
In stock: 5

Automate the Boring Stuff
Author: Al Sweigart
Price: $29.99
In stock: 3

Learning Python
Author: Mark Lutz
Price: $54.99
In stock: 2
Sold 2 book(s) for $79.98
Not enough stock! Only 3 available.
Restocked 10 books. New quantity: 13
Sold 4 book(s) for $159.96

Working with Multiple Objects

One of the powerful aspects of OOP is that each object is independent. Let’s see this in action:

class Book:
    def __init__(self, title, price):
        self.title = title
        self.price = price
        self.sales_count = 0

    def record_sale(self):
        self.sales_count += 1
        print(f"'{self.title}' has been sold {self.sales_count} time(s)")

# Create multiple book objects
book1 = Book("Python Basics", 29.99)
book2 = Book("Advanced Python", 44.99)

# Record sales for different books
book1.record_sale()
book1.record_sale()
book1.record_sale()

book2.record_sale()

print(f"\nTotal sales for '{book1.title}': {book1.sales_count}")
print(f"Total sales for '{book2.title}': {book2.sales_count}")

Output:

'Python Basics' has been sold 1 time(s)
'Python Basics' has been sold 2 time(s)
'Python Basics' has been sold 3 time(s)
'Advanced Python' has been sold 1 time(s)

Total sales for 'Python Basics': 3
Total sales for 'Advanced Python': 1

Each object maintains its own sales_count, completely independent of other objects.

Default Values in __init__

You can provide default values for attributes in the __init__ method:

class Book:
    def __init__(self, title, author, price=19.99, in_stock=True):
        self.title = title
        self.author = author
        self.price = price
        self.in_stock = in_stock

    def display_info(self):
        stock_status = "Available" if self.in_stock else "Out of stock"
        print(f"{self.title} - ${self.price} ({stock_status})")

# Create books with different combinations of arguments
book1 = Book("Python Basics", "John Doe")
book2 = Book("Advanced Python", "Jane Smith", 49.99)
book3 = Book("Python Reference", "Bob Johnson", 34.99, False)

book1.display_info()
book2.display_info()
book3.display_info()

Output:

Python Basics - $19.99 (Available)
Advanced Python - $49.99 (Available)
Python Reference - $34.99 (Out of stock)

Exercise: Build a Simple Shopping Cart

Now let’s create a ShoppingCart class that can hold books and calculate the total price:

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

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

    def add_book(self, book):
        self.books.append(book)
        print(f"Added '{book.title}' to cart")

    def remove_book(self, title):
        for book in self.books:
            if book.title == title:
                self.books.remove(book)
                print(f"Removed '{title}' from cart")
                return True
        print(f"'{title}' not found in cart")
        return False

    def get_total(self):
        total = 0
        for book in self.books:
            total += book.price
        return total

    def display_cart(self):
        if not self.books:
            print("Your cart is empty")
            return

        print("\n--- Shopping Cart ---")
        for book in self.books:
            print(f"  {book.title}: ${book.price}")
        print(f"Total: ${self.get_total():.2f}")
        print("--------------------")

# Create books
book1 = Book("Python Crash Course", 39.99)
book2 = Book("Automate the Boring Stuff", 29.99)
book3 = Book("Learning Python", 54.99)

# Create and use shopping cart
cart = ShoppingCart()
cart.add_book(book1)
cart.add_book(book2)
cart.add_book(book3)
cart.display_cart()

cart.remove_book("Automate the Boring Stuff")
cart.display_cart()

Output:

Added 'Python Crash Course' to cart
Added 'Automate the Boring Stuff' to cart
Added 'Learning Python' to cart

--- Shopping Cart ---
  Python Crash Course: $39.99
  Automate the Boring Stuff: $29.99
  Learning Python: $54.99
Total: $124.97
--------------------
Removed 'Automate the Boring Stuff' from cart

--- Shopping Cart ---
  Python Crash Course: $39.99
  Learning Python: $54.99
Total: $94.98
--------------------

Summary

In this lesson, we learned the fundamentals of object-oriented programming:

  • Classes are blueprints for creating objects
  • Objects are instances of classes with their own data
  • Attributes store data in objects (accessed with self.attribute_name)
  • Methods are functions that belong to a class and work with object data
  • The __init__ method initializes new objects
  • self refers to the specific object instance being worked with

Object-oriented programming helps us organize code by grouping related data and functions together. Instead of having separate variables for book titles, authors, and prices, we can create Book objects that bundle everything together.

In the next lesson, we’ll learn about class methods, static methods, properties, and encapsulation—tools that give us even more control over how our classes work.