Day 10 of 80

Object-Oriented Programming I: Classes

Phase 1: Python Foundation

What You'll Build Today

Welcome to Day 10! Today marks a massive shift in your programming journey. Up until now, we have been writing "scripts"—lists of instructions that run from top to bottom. This works great for small tasks, but as we move toward building complex AI agents, scripts become messy and hard to manage.

Today, we are introducing Object-Oriented Programming (OOP). This is the standard way modern software is structured.

We are going to build a User Management System. Instead of just having loose variables floating around, you will create a structured blueprint for a "User" that bundles their data (name, email) with their actions (updating email, displaying info).

Here is what you will master today:

* Classes vs. Instances: You will learn the difference between the blueprint (the design) and the house (the actual object).

* The __init__ Constructor: You will learn how to automatically set up an object the moment it is created.

* The self Parameter: You will finally understand this mysterious keyword that appears everywhere in Python code.

* Attributes and Methods: You will learn how to store data inside an object and write functions that belong specifically to that object.

Why is this crucial for AI?

When you start using frameworks like LangChain or LlamaIndex later in this bootcamp, you won't just be writing functions. You will be creating "Agents" and "Chains." These are all Objects. If you don't understand Classes, those frameworks will look like black magic. Today, we demystify the magic.

---

The Problem

Let's imagine you are building a simple system to track users for an AI application. You need to store their name, their email, and their role (e.g., Admin or Viewer). You also need a way to print their details.

With the tools you currently have (variables, lists, and dictionaries), you might write something like this:

# User 1

user1_name = "Alice"

user1_email = "alice@example.com"

user1_role = "Admin"

# User 2

user2_name = "Bob"

user2_email = "bob@example.com"

user2_role = "Viewer"

def print_user_info(name, email, role):

print(f"User: {name} | Email: {email} | Role: {role}")

print_user_info(user1_name, user1_email, user1_role)

This is already getting annoying. We have to create three separate variables for every single user. If we have 100 users, we have 300 variables.

"Okay," you might think, "I'll use a dictionary to group them!"

user1 = {

"name": "Alice",

"email": "alice@example.com",

"role": "Admin"

}

user2 = {

"name": "Bob",

"email": "bob@gmail.com",

"role": "Viewer"

}

def print_user_info(user_dict):

# We have to trust that the dictionary has the right keys!

print(f"User: {user_dict['name']} | Email: {user_dict['email']}")

print_user_info(user1)

This looks better, but it is fragile.

  • Typo Danger: What if for User 2, you accidentally typed "nmae": "Bob" instead of "name"? The function would crash because it can't find the key.
  • Disconnected Logic: The data (the dictionary) and the logic (the function) are separate. You have to manually pass the data to the function every time.
  • Scaling Issues: What if you want to update an email? You have to remember to write code that modifies the specific dictionary key.
  • As your AI programs get bigger, managing data this way becomes a nightmare. You end up with dozens of dictionaries and functions, hoping you pass the right dictionary to the right function.

    There has to be a way to bundle the data and the functions that use that data into one neat package.

    ---

    Let's Build It

    The solution is Classes.

    Think of a Class as a blueprint or a cookie cutter. It defines the shape and rules.

    Think of an Instance (or Object) as the actual house or the cookie. You can make as many cookies as you want from one cutter.

    Step 1: Defining the Blueprint

    We define a class using the class keyword. By convention, class names in Python use CapitalizedWords (PascalCase).

    class User:
    

    pass # 'pass' just means "do nothing for now"

    # This creates an instance of the class

    my_user = User()

    print(my_user)

    Output:
    <__main__.User object at 0x000001...>
    

    This output tells us we successfully created a User object. It's empty right now, but it exists in memory.

    Step 2: The Setup (__init__)

    When we create a new user, we want to force them to have a name, email, and role immediately. We don't want an empty user.

    We use a special method called __init__. This stands for "initialization." It runs automatically the moment you create a new instance.

    class User:
    

    def __init__(self, name, email, role):

    print("A new user is being created!")

    self.name = name

    self.email = email

    self.role = role

    # Creating instances

    user1 = User("Alice", "alice@example.com", "Admin")

    user2 = User("Bob", "bob@example.com", "Viewer")

    Output:
    A new user is being created!
    

    A new user is being created!

    Why this matters:

    This guarantees consistency. You cannot create a User without providing the required data. The blueprint demands it.

    Step 3: Understanding self

    You noticed self in the code above. This is the hardest concept for beginners, so let's simplify it.

    When you write the blueprint (Class), you don't know the actual name of the variable the user will create later. You don't know if they will call it user1, admin_user, or my_best_friend.

    self is a placeholder that means: "This specific object right here."

    * self.name = name translates to: "Take the name provided, and save it to this specific object's internal memory."

    If we access the data now:

    print(user1.name)
    

    print(user2.name)

    Output:
    Alice
    

    Bob

    Because of self, user1 knows its name is Alice, and user2 knows its name is Bob, even though they were made from the same class.

    Step 4: Adding Behaviors (Methods)

    Functions defined inside a class are called Methods. They look exactly like normal functions, but the first parameter must always be self. This allows the method to access the specific data of the object calling it.

    Let's move our print_user_info logic inside the class.

    class User:
    

    def __init__(self, name, email, role):

    self.name = name

    self.email = email

    self.role = role

    # This is a method

    def display_info(self):

    # We don't need to pass arguments! We already have them in 'self'.

    print(f"User: {self.name} | Role: {self.role}")

    user1 = User("Alice", "alice@example.com", "Admin")

    # Notice we don't pass 'self' or arguments here. Python handles it.

    user1.display_info()

    Output:
    User: Alice | Role: Admin
    
    Why this matters:

    The function display_info is now attached to the data. You can't lose it. You don't need to pass arguments to it because the object already holds the data in self.

    Step 5: Methods that Modify Data

    Methods can also change the object's data. Let's add a method to update the email.

    class User:
    

    def __init__(self, name, email, role):

    self.name = name

    self.email = email

    self.role = role

    def display_info(self):

    print(f"User: {self.name} | Email: {self.email}")

    def update_email(self, new_email):

    print(f"Updating email for {self.name}...")

    self.email = new_email

    print("Email updated.")

    # 1. Create User

    user1 = User("Alice", "old_email@test.com", "Admin")

    user1.display_info()

    # 2. Update Email

    user1.update_email("new_alice@test.com")

    # 3. Verify Change

    user1.display_info()

    Output:
    User: Alice | Email: old_email@test.com
    

    Updating email for Alice...

    Email updated.

    User: Alice | Email: new_alice@test.com

    This is the power of OOP. The User object is a self-contained unit that manages its own data and behavior.

    ---

    Now You Try

    Take the User class code from Step 5 above and extend it.

  • Add an Attribute: Update the __init__ method to accept a location (e.g., "New York", "London") and store it in self.location.
  • Update Display: Modify display_info so it also prints the user's location.
  • Add a Method: Create a new method called change_role(self, new_role). It should update self.role to the new value and print a confirmation message like "Alice is now a SuperAdmin".
  • Run your code to ensure you can create a user, see their location, and change their role.

    ---

    Challenge Project: The ChatBot Class

    Now we will apply this to an AI context. You are going to build a ChatBot class. This mimics how you might set up an AI agent with a specific "system personality."

    Requirements:
  • Create a class named ChatBot.
  • The __init__ method should take three arguments: name (e.g., "Botty"), personality (e.g., "grumpy", "helpful"), and model_version (e.g., "1.0").
  • Create a method called introduce_self(). It should print: "Hello, I am [name]. I am a [personality] bot running on version [model_version]."
  • Create a method called respond(user_message).
  • * It should take a string user_message as an input.

    * It should print a response that combines the bot's name and the message.

    * Example format: [Botty says]: Interesting that you said "[user_message]"

  • Bonus Logic: Inside respond, check the bot's personality. If the personality is "grumpy", add "I don't want to talk right now, but..." to the start of the response.
  • Example Input/Output:
    # Setup
    

    my_bot = ChatBot("Robo", "grumpy", "v2")

    # Action

    my_bot.introduce_self()

    my_bot.respond("What is the weather?")

    Expected Output:
    Hello, I am Robo. I am a grumpy bot running on version v2.
    

    [Robo says]: I don't want to talk right now, but... Interesting that you said "What is the weather?"

    Hints:

    * Remember to use self.variable_name when accessing attributes inside your methods.

    * You can use if statements inside methods just like normal functions.

    ---

    What You Learned

    Today you crossed the bridge from scripting to engineering.

    * Classes are the blueprints (The recipe).

    * Instances are the objects created from blueprints (The cake).

    * __init__ is the setup function that runs automatically.

    * self allows the object to refer to its own specific data.

    * Methods are functions that live inside the class and act on that data.

    Why This Matters for AI:

    In the coming weeks, you will use a library called OpenAI. You will write code like this:

    client = OpenAI(api_key="...").

    You are creating an Instance of the OpenAI Class.

    Later, you will use LangChain:

    agent = initialize_agent(tools, llm, ...)

    You are creating an Agent object.

    Understanding that these tools are just objects with attributes (configuration) and methods (actions) gives you the power to not just use them, but to understand how they work under the hood.

    Tomorrow: We will deepen our OOP skills. What happens if you want a SuperUser that does everything a User does, but also has special powers? You don't want to copy-paste the code. You will learn Inheritance to solve this.