Skip to main content

🏗️ Lesson 6.1: Classes and Objects

So far, you've used built-in data types — strings, numbers, lists, dictionaries. But what if you need a custom type? A Player with a name and health? A BankAccount with a balance and withdrawal rules? That's what classes are for. They let you create your own blueprints for data and behavior.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why object-oriented programming (OOP) exists and when it helps
  • Define a class and create objects (instances) from it
  • Add attributes (data) and methods (behavior) to a class
  • Understand self / this — the reference to the current object
  • Write a constructor that initializes objects with starting data
  • Create multiple objects from the same class with different data

Estimated Time: 60 minutes

Project: Build a Player class for a simple RPG inventory system

📑 In This Lesson

Why Object-Oriented Programming?

Until now, your data and your functions have lived separately. You might have a dictionary for a player and a function that operates on it:

# Data — just a dictionary
player = {"name": "Alice", "health": 100, "inventory": []}

# Functions that operate on that data
def take_damage(player, amount):
    player["health"] -= amount
    if player["health"] < 0:
        player["health"] = 0

def add_item(player, item):
    player["inventory"].append(item)

take_damage(player, 30)
add_item(player, "sword")
print(f"{player['name']}: {player['health']} HP")  # Alice: 70 HP
// Data — just an object
let player = { name: "Alice", health: 100, inventory: [] };

// Functions that operate on that data
function takeDamage(player, amount) {
    player.health -= amount;
    if (player.health < 0) player.health = 0;
}

function addItem(player, item) {
    player.inventory.push(item);
}

takeDamage(player, 30);
addItem(player, "sword");
console.log(`${player.name}: ${player.health} HP`);  // Alice: 70 HP
// Without OOP, C# would use static methods + arrays/dicts
// But C# is built for OOP — this is the "fighting the language" way

// You'd have to use dictionaries or parallel arrays
// and pass them into every function. It gets messy fast.

This works for small programs, but it has problems as your code grows:

Problem Why It Matters
Data and behavior are separated You have to remember which functions go with which data. Nothing enforces it.
No structure guarantee Any code could set player["health"] = "banana" — no guardrails.
Hard to reuse Creating 50 enemies means 50 dictionaries with no guarantee they all have the right keys.
Difficult to scale As features grow, you end up with dozens of loose functions and no clear organization.

OOP solves this by bundling data and behavior together into a single unit called a class.

🎓 Instructor Note: Delivery Guidance

Start by showing the "before" code above and asking: "What happens if we misspell a key? What if we forget to initialize inventory?" Let students feel the pain of loose data before introducing classes as the solution. The RPG/game metaphor works extremely well here — students immediately grasp "a blueprint for creating characters." C# students will feel right at home since the language was designed around OOP. Python and JS students may need more motivation since those languages work fine without classes for small programs.

Classes vs. Objects

These two terms get used a lot, so let's nail them down:

🏗️ Class = Blueprint

A class defines what something is — what data it holds and what it can do. Think of it like a cookie cutter or an architect's blueprint. It's not a cookie or a house — it's the plan for making them.

🎯 Object = Instance

An object (also called an instance) is a specific thing created from a class. From a single Player class, you can create player1, player2, player3 — each with their own name, health, and inventory.

graph LR A["🏗️ Class: Player
name, health, inventory
take_damage(), add_item()"] A -->|"new"| B["🎮 player1
Alice, 100 HP
[sword]"] A -->|"new"| C["🎮 player2
Bob, 85 HP
[shield, potion]"] A -->|"new"| D["🎮 player3
Charlie, 100 HP
[]"] style A fill:#3b82f6,stroke:#2563eb,color:#fff style B fill:#22c55e,stroke:#16a34a,color:#fff style C fill:#22c55e,stroke:#16a34a,color:#fff style D fill:#22c55e,stroke:#16a34a,color:#fff
Concept Real-World Analogy Code Example
Class Cookie cutter / Blueprint / Recipe class Player
Object A specific cookie / A built house / A baked cake player1 = Player("Alice", 100)
Attribute Color of the house / Flavor of the cake player1.name, player1.health
Method Opening the door / Slicing the cake player1.take_damage(30)

Defining a Class

Let's start with the simplest possible class — one with just a constructor that stores some data.

# Define the class
class Player:
    pass  # Empty class (placeholder)

# Create objects (instances)
player1 = Player()
player2 = Player()

# They're separate objects
print(player1)         # <__main__.Player object at 0x...>
print(player1 == player2)  # False — different objects!
print(type(player1))   # <class '__main__.Player'>
// Define the class
class Player {
    // Empty class (placeholder)
}

// Create objects (instances)
let player1 = new Player();
let player2 = new Player();

// They're separate objects
console.log(player1);              // Player {}
console.log(player1 === player2);  // false — different objects!
console.log(typeof player1);       // "object"
console.log(player1 instanceof Player);  // true
// Define the class
class Player
{
    // Empty class (placeholder)
}

// Create objects (instances)
Player player1 = new Player();
Player player2 = new Player();

// They're separate objects
Console.WriteLine(player1);                    // Player
Console.WriteLine(player1 == player2);         // False
Console.WriteLine(player1.GetType().Name);     // Player

⚠️ Naming Convention: Classes Use PascalCase

In all three languages, class names start with a capital letter and use PascalCase: Player, BankAccount, ShoppingCart. Variables holding instances use camelCase (JS/C#) or snake_case (Python): player1, myAccount, shopping_cart.

Syntax Comparison

Feature 🐍 Python ⚡ JavaScript 🔷 C#
Define a class class Player: class Player { } class Player { }
Create an instance Player() new Player() new Player()
Keyword new? No Yes Yes
Braces or indentation? Indentation Braces { } Braces { }
🎓 Instructor Note: Delivery Guidance

Highlight that Python doesn't use new — calling Player() looks just like calling a function. JS and C# both require new. This is a great time to reinforce the JS/C# "syntax siblings" theme from earlier modules. Also note: the empty class is useless on its own — it's just to show the minimal syntax. The constructor in the next section is where it gets useful.

Constructors: Initializing Objects

A constructor is a special method that runs automatically when you create an object. It sets up the object's initial data (attributes). Every language has its own syntax for this.

class Player:
    def __init__(self, name, health=100):
        self.name = name          # Instance attribute
        self.health = health      # Instance attribute with default
        self.inventory = []       # Always starts empty

# Create players
player1 = Player("Alice")
player2 = Player("Bob", 85)

print(player1.name)       # Alice
print(player1.health)     # 100
print(player2.health)     # 85
print(player1.inventory)  # []
class Player {
    constructor(name, health = 100) {
        this.name = name;          // Instance property
        this.health = health;      // Instance property with default
        this.inventory = [];       // Always starts empty
    }
}

// Create players
let player1 = new Player("Alice");
let player2 = new Player("Bob", 85);

console.log(player1.name);       // Alice
console.log(player1.health);     // 100
console.log(player2.health);     // 85
console.log(player1.inventory);  // []
class Player
{
    public string Name;
    public int Health;
    public List<string> Inventory;

    // Constructor — same name as the class
    public Player(string name, int health = 100)
    {
        Name = name;
        Health = health;
        Inventory = new List<string>();
    }
}

// Create players
Player player1 = new Player("Alice");
Player player2 = new Player("Bob", 85);

Console.WriteLine(player1.Name);       // Alice
Console.WriteLine(player1.Health);     // 100
Console.WriteLine(player2.Health);     // 85
Console.WriteLine(player1.Inventory.Count);  // 0

Constructor Syntax Comparison

Feature 🐍 Python ⚡ JavaScript 🔷 C#
Constructor name __init__ constructor Same as class name
Self reference self (explicit first param) this (implicit) this (implicit, often omitted)
Set attribute self.name = name this.name = name; Name = name;
Default params health=100 health = 100 int health = 100

💡 Python's self — The Explicit Difference

Python is unique: you must write self as the first parameter of every method. JavaScript and C# provide this automatically — you never list it as a parameter. This is one of the biggest syntax differences in OOP across these three languages.

Methods: Adding Behavior

Attributes store data. Methods are functions defined inside a class that define what the object can do. Methods can read and modify the object's own attributes.

class Player:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
        self.inventory = []

    def take_damage(self, amount):
        self.health -= amount
        if self.health < 0:
            self.health = 0
        print(f"{self.name} takes {amount} damage! HP: {self.health}")

    def heal(self, amount):
        self.health += amount
        if self.health > 100:
            self.health = 100
        print(f"{self.name} heals {amount}! HP: {self.health}")

    def add_item(self, item):
        self.inventory.append(item)
        print(f"{self.name} picked up {item}")

    def show_status(self):
        items = ", ".join(self.inventory) if self.inventory else "empty"
        print(f"{self.name} | HP: {self.health} | Items: {items}")

# Use it
hero = Player("Alice")
hero.add_item("sword")
hero.add_item("potion")
hero.take_damage(35)
hero.heal(20)
hero.show_status()
# Alice | HP: 85 | Items: sword, potion
class Player {
    constructor(name, health = 100) {
        this.name = name;
        this.health = health;
        this.inventory = [];
    }

    takeDamage(amount) {
        this.health -= amount;
        if (this.health < 0) this.health = 0;
        console.log(`${this.name} takes ${amount} damage! HP: ${this.health}`);
    }

    heal(amount) {
        this.health += amount;
        if (this.health > 100) this.health = 100;
        console.log(`${this.name} heals ${amount}! HP: ${this.health}`);
    }

    addItem(item) {
        this.inventory.push(item);
        console.log(`${this.name} picked up ${item}`);
    }

    showStatus() {
        let items = this.inventory.length ? this.inventory.join(", ") : "empty";
        console.log(`${this.name} | HP: ${this.health} | Items: ${items}`);
    }
}

// Use it
let hero = new Player("Alice");
hero.addItem("sword");
hero.addItem("potion");
hero.takeDamage(35);
hero.heal(20);
hero.showStatus();
// Alice | HP: 85 | Items: sword, potion
class Player
{
    public string Name;
    public int Health;
    public List<string> Inventory;

    public Player(string name, int health = 100)
    {
        Name = name;
        Health = health;
        Inventory = new List<string>();
    }

    public void TakeDamage(int amount)
    {
        Health -= amount;
        if (Health < 0) Health = 0;
        Console.WriteLine($"{Name} takes {amount} damage! HP: {Health}");
    }

    public void Heal(int amount)
    {
        Health += amount;
        if (Health > 100) Health = 100;
        Console.WriteLine($"{Name} heals {amount}! HP: {Health}");
    }

    public void AddItem(string item)
    {
        Inventory.Add(item);
        Console.WriteLine($"{Name} picked up {item}");
    }

    public void ShowStatus()
    {
        string items = Inventory.Count > 0
            ? string.Join(", ", Inventory) : "empty";
        Console.WriteLine($"{Name} | HP: {Health} | Items: {items}");
    }
}

// Use it
Player hero = new Player("Alice");
hero.AddItem("sword");
hero.AddItem("potion");
hero.TakeDamage(35);
hero.Heal(20);
hero.ShowStatus();
// Alice | HP: 85 | Items: sword, potion

✅ Notice the Pattern

Compare the "before OOP" code at the top of this lesson with the class version. Instead of take_damage(player, 30), you write hero.take_damage(30). The data and behavior live together — the object knows how to operate on itself. This is the core idea of OOP.

Method Syntax Comparison

Feature 🐍 Python ⚡ JavaScript 🔷 C#
Define a method def heal(self, amount): heal(amount) { } public void Heal(int amount) { }
Call a method hero.heal(20) hero.heal(20) hero.Heal(20)
function keyword? def No (inside class) No (return type instead)
Access modifier? Not required Not required public / private
🎓 Instructor Note: Delivery Guidance

Walk through building the class method by method, not all at once. Start with just __init__, create an object, access its attributes. Then add take_damage, test it. Then heal, test it. This incremental approach lets students see how each piece works before the full class appears. Emphasize: methods are just functions that live inside a class and have access to self/this.

Understanding self / this

The trickiest concept for beginners: when a method runs, how does it know which object's data to use? That's what self (Python) and this (JS/C#) do — they're a reference to the specific object that called the method.

class Dog:
    def __init__(self, name):
        self.name = name    # self = the object being created

    def bark(self):
        # self = the object calling this method
        print(f"{self.name} says Woof!")

fido = Dog("Fido")
rex = Dog("Rex")

fido.bark()   # "Fido says Woof!"  — self is fido
rex.bark()    # "Rex says Woof!"   — self is rex

# What Python actually does behind the scenes:
# fido.bark()  →  Dog.bark(fido)
# rex.bark()   →  Dog.bark(rex)
class Dog {
    constructor(name) {
        this.name = name;    // this = the object being created
    }

    bark() {
        // this = the object calling this method
        console.log(`${this.name} says Woof!`);
    }
}

let fido = new Dog("Fido");
let rex = new Dog("Rex");

fido.bark();   // "Fido says Woof!"  — this is fido
rex.bark();    // "Rex says Woof!"   — this is rex

// Note: 'this' in JavaScript can be tricky in callbacks
// (we'll cover that in Lesson 17)
class Dog
{
    public string Name;

    public Dog(string name)
    {
        Name = name;   // 'this' is implicit
        // this.Name = name;  ← also valid, but not needed here
    }

    public void Bark()
    {
        // 'this' is the object calling the method
        Console.WriteLine($"{Name} says Woof!");
        // Console.WriteLine($"{this.Name} says Woof!");  ← same thing
    }
}

Dog fido = new Dog("Fido");
Dog rex = new Dog("Rex");

fido.Bark();   // "Fido says Woof!"  — this is fido
rex.Bark();    // "Rex says Woof!"   — this is rex
sequenceDiagram participant Code as Your Code participant fido as fido (Dog) participant rex as rex (Dog) Code->>fido: fido.bark() Note right of fido: self/this = fido
name = "Fido" fido-->>Code: "Fido says Woof!" Code->>rex: rex.bark() Note right of rex: self/this = rex
name = "Rex" rex-->>Code: "Rex says Woof!"

💡 Key Insight

self / this is like a pronoun. When you write self.name, you're saying "my name" — where "my" refers to whichever specific object is running the method. The same method code runs on different data depending on which object calls it.

Multiple Objects, One Blueprint

The whole point of classes is creating multiple objects that share structure but have independent data.

class Player:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
        self.inventory = []

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

    def show_status(self):
        items = ", ".join(self.inventory) if self.inventory else "empty"
        print(f"{self.name} | HP: {self.health} | Items: {items}")

# Create three independent players
alice = Player("Alice")
bob = Player("Bob", 85)
charlie = Player("Charlie")

# Each has their OWN data
alice.add_item("sword")
bob.add_item("shield")
bob.add_item("potion")
charlie.add_item("bow")

alice.show_status()    # Alice | HP: 100 | Items: sword
bob.show_status()      # Bob | HP: 85 | Items: shield, potion
charlie.show_status()  # Charlie | HP: 100 | Items: bow

# Modifying one doesn't affect the others!
alice.health = 50
print(bob.health)  # 85 — unchanged
class Player {
    constructor(name, health = 100) {
        this.name = name;
        this.health = health;
        this.inventory = [];
    }

    addItem(item) { this.inventory.push(item); }

    showStatus() {
        let items = this.inventory.length ? this.inventory.join(", ") : "empty";
        console.log(`${this.name} | HP: ${this.health} | Items: ${items}`);
    }
}

// Create three independent players
let alice = new Player("Alice");
let bob = new Player("Bob", 85);
let charlie = new Player("Charlie");

// Each has their OWN data
alice.addItem("sword");
bob.addItem("shield");
bob.addItem("potion");
charlie.addItem("bow");

alice.showStatus();    // Alice | HP: 100 | Items: sword
bob.showStatus();      // Bob | HP: 85 | Items: shield, potion
charlie.showStatus();  // Charlie | HP: 100 | Items: bow

// Modifying one doesn't affect the others!
alice.health = 50;
console.log(bob.health);  // 85 — unchanged
class Player
{
    public string Name;
    public int Health;
    public List<string> Inventory;

    public Player(string name, int health = 100)
    {
        Name = name;
        Health = health;
        Inventory = new List<string>();
    }

    public void AddItem(string item) => Inventory.Add(item);

    public void ShowStatus()
    {
        string items = Inventory.Count > 0
            ? string.Join(", ", Inventory) : "empty";
        Console.WriteLine($"{Name} | HP: {Health} | Items: {items}");
    }
}

// Create three independent players
Player alice = new Player("Alice");
Player bob = new Player("Bob", 85);
Player charlie = new Player("Charlie");

// Each has their OWN data
alice.AddItem("sword");
bob.AddItem("shield");
bob.AddItem("potion");
charlie.AddItem("bow");

alice.ShowStatus();    // Alice | HP: 100 | Items: sword
bob.ShowStatus();      // Bob | HP: 85 | Items: shield, potion
charlie.ShowStatus();  // Charlie | HP: 100 | Items: bow

// Modifying one doesn't affect the others!
alice.Health = 50;
Console.WriteLine(bob.Health);  // 85 — unchanged

Objects in Collections

Objects are values — you can store them in lists, loop over them, filter them, and pass them to functions. This is where OOP starts to feel powerful.

class Player:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
        self.inventory = []

    def is_alive(self):
        return self.health > 0

    def show_status(self):
        status = "alive" if self.is_alive() else "defeated"
        print(f"  {self.name}: {self.health} HP ({status})")

# Create a party of adventurers
party = [
    Player("Alice", 100),
    Player("Bob", 45),
    Player("Charlie", 0),
    Player("Diana", 78)
]

# Loop over objects
print("=== Party Status ===")
for player in party:
    player.show_status()

# Filter: who's still alive?
alive = [p for p in party if p.is_alive()]
print(f"\nSurvivors: {[p.name for p in alive]}")
# Survivors: ['Alice', 'Bob', 'Diana']

# Find: who has the lowest health (among the living)?
weakest = min(alive, key=lambda p: p.health)
print(f"Needs healing: {weakest.name} ({weakest.health} HP)")
# Needs healing: Bob (45 HP)

# Total party health
total_hp = sum(p.health for p in party)
print(f"Total party HP: {total_hp}")  # 223
class Player {
    constructor(name, health = 100) {
        this.name = name;
        this.health = health;
        this.inventory = [];
    }

    isAlive() { return this.health > 0; }

    showStatus() {
        let status = this.isAlive() ? "alive" : "defeated";
        console.log(`  ${this.name}: ${this.health} HP (${status})`);
    }
}

// Create a party of adventurers
let party = [
    new Player("Alice", 100),
    new Player("Bob", 45),
    new Player("Charlie", 0),
    new Player("Diana", 78)
];

// Loop over objects
console.log("=== Party Status ===");
party.forEach(p => p.showStatus());

// Filter: who's still alive?
let alive = party.filter(p => p.isAlive());
console.log(`\nSurvivors: ${alive.map(p => p.name)}`);

// Find: lowest health among the living
let weakest = alive.reduce((min, p) => p.health < min.health ? p : min);
console.log(`Needs healing: ${weakest.name} (${weakest.health} HP)`);

// Total party health
let totalHP = party.reduce((sum, p) => sum + p.health, 0);
console.log(`Total party HP: ${totalHP}`);  // 223
class Player
{
    public string Name;
    public int Health;

    public Player(string name, int health = 100)
    {
        Name = name;
        Health = health;
    }

    public bool IsAlive() => Health > 0;

    public void ShowStatus()
    {
        string status = IsAlive() ? "alive" : "defeated";
        Console.WriteLine($"  {Name}: {Health} HP ({status})");
    }
}

// Create a party of adventurers
var party = new List<Player>
{
    new Player("Alice", 100),
    new Player("Bob", 45),
    new Player("Charlie", 0),
    new Player("Diana", 78)
};

// Loop over objects
Console.WriteLine("=== Party Status ===");
party.ForEach(p => p.ShowStatus());

// Filter: who's still alive?
var alive = party.Where(p => p.IsAlive()).ToList();
Console.WriteLine($"\nSurvivors: {string.Join(", ", alive.Select(p => p.Name))}");

// Find: lowest health among the living
var weakest = alive.OrderBy(p => p.Health).First();
Console.WriteLine($"Needs healing: {weakest.Name} ({weakest.Health} HP)");

// Total party health
int totalHP = party.Sum(p => p.Health);
Console.WriteLine($"Total party HP: {totalHP}");  // 223

✅ Collections + OOP = Real Programs

This is exactly how real applications work: a shopping cart is a list of Product objects, a chat app stores a list of Message objects, a game manages a list of Entity objects. The collection patterns you learned in Module 5 (filter, map, reduce, find) apply directly to lists of your own objects.

🎓 Instructor Note: Delivery Guidance

This section ties Module 5 directly to Module 6. Point out that party.filter(p => p.isAlive()) is the same pattern as scores.filter(s => s >= 70) — just with objects instead of numbers. Students who were comfortable with filter/map/reduce should see this as a natural extension, not something new. The RPG party example is engaging and immediately relatable.

Exercises

🏋️ Exercise 1: BankAccount Class

Objective: Create a BankAccount class with:

  • Constructor that takes owner (string) and optional balance (default 0)
  • deposit(amount) — adds to balance, prints confirmation
  • withdraw(amount) — subtracts from balance, but rejects if insufficient funds
  • get_balance() / getBalance() — returns the current balance

Test with two accounts performing deposits and withdrawals.

✅ Solution
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit must be positive!")
            return
        self.balance += amount
        print(f"{self.owner}: Deposited ${amount:.2f}. Balance: ${self.balance:.2f}")

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal must be positive!")
            return
        if amount > self.balance:
            print(f"{self.owner}: Insufficient funds! Balance: ${self.balance:.2f}")
            return
        self.balance -= amount
        print(f"{self.owner}: Withdrew ${amount:.2f}. Balance: ${self.balance:.2f}")

    def get_balance(self):
        return self.balance

# Test
acct1 = BankAccount("Alice", 1000)
acct2 = BankAccount("Bob")

acct1.deposit(500)       # Alice: Deposited $500.00. Balance: $1500.00
acct1.withdraw(200)      # Alice: Withdrew $200.00. Balance: $1300.00
acct2.deposit(100)       # Bob: Deposited $100.00. Balance: $100.00
acct2.withdraw(150)      # Bob: Insufficient funds! Balance: $100.00
class BankAccount {
    constructor(owner, balance = 0) {
        this.owner = owner;
        this.balance = balance;
    }

    deposit(amount) {
        if (amount <= 0) { console.log("Deposit must be positive!"); return; }
        this.balance += amount;
        console.log(`${this.owner}: Deposited $${amount.toFixed(2)}. Balance: $${this.balance.toFixed(2)}`);
    }

    withdraw(amount) {
        if (amount <= 0) { console.log("Withdrawal must be positive!"); return; }
        if (amount > this.balance) {
            console.log(`${this.owner}: Insufficient funds! Balance: $${this.balance.toFixed(2)}`);
            return;
        }
        this.balance -= amount;
        console.log(`${this.owner}: Withdrew $${amount.toFixed(2)}. Balance: $${this.balance.toFixed(2)}`);
    }

    getBalance() { return this.balance; }
}

// Test
let acct1 = new BankAccount("Alice", 1000);
let acct2 = new BankAccount("Bob");

acct1.deposit(500);       // Alice: Deposited $500.00. Balance: $1500.00
acct1.withdraw(200);      // Alice: Withdrew $200.00. Balance: $1300.00
acct2.deposit(100);       // Bob: Deposited $100.00. Balance: $100.00
acct2.withdraw(150);      // Bob: Insufficient funds! Balance: $100.00
class BankAccount
{
    public string Owner;
    public decimal Balance;

    public BankAccount(string owner, decimal balance = 0)
    {
        Owner = owner;
        Balance = balance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0) { Console.WriteLine("Deposit must be positive!"); return; }
        Balance += amount;
        Console.WriteLine($"{Owner}: Deposited ${Balance:F2}. Balance: ${Balance:F2}");
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0) { Console.WriteLine("Withdrawal must be positive!"); return; }
        if (amount > Balance)
        {
            Console.WriteLine($"{Owner}: Insufficient funds! Balance: ${Balance:F2}");
            return;
        }
        Balance -= amount;
        Console.WriteLine($"{Owner}: Withdrew ${amount:F2}. Balance: ${Balance:F2}");
    }

    public decimal GetBalance() => Balance;
}

// Test
BankAccount acct1 = new BankAccount("Alice", 1000);
BankAccount acct2 = new BankAccount("Bob");

acct1.Deposit(500);       // Alice: Deposited $500.00. Balance: $1500.00
acct1.Withdraw(200);      // Alice: Withdrew $200.00. Balance: $1300.00
acct2.Deposit(100);       // Bob: Deposited $100.00. Balance: $100.00
acct2.Withdraw(150);      // Bob: Insufficient funds! Balance: $100.00

🏋️ Exercise 2: RPG Party Manager

Objective: Using the Player class from this lesson, write code that:

  • Creates a party of 4 players with different names and health values
  • Gives each player at least one inventory item
  • Simulates a battle: deal random damage (10–40) to each player
  • After the battle, prints: who survived, who was defeated, total party HP remaining
  • Finds the player with the most items in their inventory
✅ Solution
import random

class Player:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
        self.inventory = []

    def take_damage(self, amount):
        self.health -= amount
        if self.health < 0:
            self.health = 0

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

    def is_alive(self):
        return self.health > 0

# Build the party
party = [
    Player("Alice", 100),
    Player("Bob", 80),
    Player("Charlie", 90),
    Player("Diana", 70)
]

# Give items
party[0].add_item("sword")
party[0].add_item("shield")
party[1].add_item("bow")
party[2].add_item("staff")
party[2].add_item("potion")
party[2].add_item("scroll")
party[3].add_item("dagger")

# Simulate battle
print("=== Battle! ===")
for player in party:
    damage = random.randint(10, 40)
    player.take_damage(damage)
    status = "survived" if player.is_alive() else "DEFEATED"
    print(f"  {player.name} took {damage} damage → {player.health} HP ({status})")

# Results
survivors = [p for p in party if p.is_alive()]
defeated = [p for p in party if not p.is_alive()]
total_hp = sum(p.health for p in party)

print(f"\nSurvivors: {[p.name for p in survivors]}")
print(f"Defeated: {[p.name for p in defeated]}")
print(f"Total HP remaining: {total_hp}")

# Most items
most_items = max(party, key=lambda p: len(p.inventory))
print(f"Most items: {most_items.name} ({len(most_items.inventory)} items)")
class Player {
    constructor(name, health = 100) {
        this.name = name;
        this.health = health;
        this.inventory = [];
    }

    takeDamage(amount) {
        this.health -= amount;
        if (this.health < 0) this.health = 0;
    }

    addItem(item) { this.inventory.push(item); }
    isAlive() { return this.health > 0; }
}

// Build the party
let party = [
    new Player("Alice", 100),
    new Player("Bob", 80),
    new Player("Charlie", 90),
    new Player("Diana", 70)
];

// Give items
party[0].addItem("sword"); party[0].addItem("shield");
party[1].addItem("bow");
party[2].addItem("staff"); party[2].addItem("potion"); party[2].addItem("scroll");
party[3].addItem("dagger");

// Simulate battle
console.log("=== Battle! ===");
for (let player of party) {
    let damage = Math.floor(Math.random() * 31) + 10;
    player.takeDamage(damage);
    let status = player.isAlive() ? "survived" : "DEFEATED";
    console.log(`  ${player.name} took ${damage} damage → ${player.health} HP (${status})`);
}

// Results
let survivors = party.filter(p => p.isAlive());
let defeated = party.filter(p => !p.isAlive());
let totalHP = party.reduce((sum, p) => sum + p.health, 0);

console.log(`\nSurvivors: ${survivors.map(p => p.name)}`);
console.log(`Defeated: ${defeated.map(p => p.name)}`);
console.log(`Total HP remaining: ${totalHP}`);

let mostItems = party.reduce((max, p) =>
    p.inventory.length > max.inventory.length ? p : max);
console.log(`Most items: ${mostItems.name} (${mostItems.inventory.length} items)`);
class Player
{
    public string Name;
    public int Health;
    public List<string> Inventory = new();

    public Player(string name, int health = 100)
    { Name = name; Health = health; }

    public void TakeDamage(int amount)
    { Health -= amount; if (Health < 0) Health = 0; }

    public void AddItem(string item) => Inventory.Add(item);
    public bool IsAlive() => Health > 0;
}

var party = new List<Player>
{
    new Player("Alice", 100), new Player("Bob", 80),
    new Player("Charlie", 90), new Player("Diana", 70)
};

party[0].AddItem("sword"); party[0].AddItem("shield");
party[1].AddItem("bow");
party[2].AddItem("staff"); party[2].AddItem("potion"); party[2].AddItem("scroll");
party[3].AddItem("dagger");

var rng = new Random();
Console.WriteLine("=== Battle! ===");
foreach (var p in party)
{
    int damage = rng.Next(10, 41);
    p.TakeDamage(damage);
    string status = p.IsAlive() ? "survived" : "DEFEATED";
    Console.WriteLine($"  {p.Name} took {damage} damage → {p.Health} HP ({status})");
}

var survivors = party.Where(p => p.IsAlive()).ToList();
var defeated = party.Where(p => !p.IsAlive()).ToList();
int totalHP = party.Sum(p => p.Health);

Console.WriteLine($"\nSurvivors: {string.Join(", ", survivors.Select(p => p.Name))}");
Console.WriteLine($"Defeated: {string.Join(", ", defeated.Select(p => p.Name))}");
Console.WriteLine($"Total HP remaining: {totalHP}");

var mostItems = party.OrderByDescending(p => p.Inventory.Count).First();
Console.WriteLine($"Most items: {mostItems.Name} ({mostItems.Inventory.Count} items)");
🎓 Instructor Note: Delivery Guidance

Exercise 1 (BankAccount) reinforces the core class mechanics: constructor, methods, guarding data with conditions. Exercise 2 (RPG Party) ties OOP to collections from Module 5 and adds randomness for engagement. Students will get different results each run, which makes it fun to compare. Challenge fast students: add a transfer(from_account, to_account, amount) function or a Player.attack(target) method that deals damage to another player.

Summary

🎉 Key Takeaways

  • OOP bundles data (attributes) and behavior (methods) into classes
  • A class is a blueprint; an object is a specific instance created from it
  • Constructors initialize objects: Python __init__, JS constructor, C# same-name-as-class
  • self / this refers to the current object — Python requires it as an explicit parameter, JS/C# provide it implicitly
  • Methods are functions that live inside a class and operate on the object's data
  • Objects are values — you can store them in lists, filter them, loop over them, and pass them to functions
  • JS and C# use new to create instances; Python does not

🚀 What's Next?

You can now create classes with attributes and methods. In the next lesson, we'll go deeper into properties, methods, and constructors — including return values from methods, string representations (__str__ / toString), and controlling access to data with public/private visibility.

🎯 Quick Check

Question 1: What is the Python constructor method called?

Question 2: Which language requires new to create an object?

Question 3: What does self (Python) or this (JS/C#) refer to inside a method?