You're tracking users in your app. Each user has a name, email, and login method. Instead of managing separate dictionaries, a class bundles the data and behavior into one reusable blueprint - cleaner and more maintainable.

Basic class definition

Create a class with attributes and methods.

basic_class.py
# Basic Class Definition

print("=== Basic Class ===\n")

# Define a simple class
class Dog:
    pass  # Empty class for now

# Create an object (instance)
my_dog = Dog()
print(f"Created: {my_dog}")
print(f"Type: {type(my_dog)}")

print("\n=== Class with Attribute ===")

class Cat:
    species = "Felis catus"  # Class attribute

# Create cats
cat1 = Cat()
cat2 = Cat()

print(f"Cat 1 species: {cat1.species}")
print(f"Cat 2 species: {cat2.species}")

# Both share the same class attribute
print(f"Same attribute? {cat1.species is cat2.species}")

print("\n=== Adding Instance Attributes ===")

# Add attributes to specific instance
cat1.name = "Whiskers"
cat2.name = "Luna"

print(f"Cat 1 name: {cat1.name}")
print(f"Cat 2 name: {cat2.name}")

print("\n=== Class vs Object ===")

print("""
Class = Blueprint/Template
  - Defines structure
  - Like a cookie cutter

Object = Instance/Reality
  - Actual data
  - Like actual cookies
""")

# Multiple objects from same class
dog1 = Dog()
dog2 = Dog()
dog3 = Dog()

print(f"dog1 is dog2? {dog1 is dog2}")  # False - different objects
print(f"All are Dogs? {type(dog1) == type(dog2) == Dog}")  # True

class Name: defines a blueprint. Methods are functions inside the class.

class Blueprint for objects. Defines attributes (data) and methods (behavior).

Instance attributes

Each object has its own copy of attributes.

attributes.py
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Instance Attributes with __init__

print("=== The __init__ Method ===\n")

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"Creating dog: {name}, age {age}")

# Create dogs
buddy = 
max_dog = Dog("Max", 5)

print(f"\n{buddy.name} is {buddy.age} years old")
print(f"{max_dog.name} is {max_dog.age} years old")

print("\n=== Default Values ===")

class Cat:
    def __init__(self, name, color="orange"):
        self.name = name
        self.color = color

# With and without default
garfield = Cat("Garfield")  # Uses default color
whiskers = Cat("Whiskers", "gray")

print(f"{garfield.name} is {garfield.color}")
print(f"{whiskers.name} is {whiskers.color}")

print("\n=== Multiple Initialization Patterns ===")

class Person:
    def __init__(self, name, age=0, city="Unknown"):
        self.name = name
        self.age = age
        self.city = city
    
    def display(self):
        print(f"{self.name}, {self.age}, from {self.city}")

# Various ways to create
p1 = Person("Alice")
p2 = Person("Bob", 25)
p3 = Person("Carol", 30, "NYC")
p4 = Person("Dave", city="LA")

p1.display()
p2.display()
p3.display()
p4.display()

print("\n=== Computed Attributes ===")

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height
        self.perimeter = 2 * (width + height)

rect = 
print(f"Rectangle {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")

Attributes set in __init__ are unique to each object.

attribute Data stored on an object: `self.name = "Alice"`. Accessed via `obj.name`.

Instance methods

Methods that operate on object data.

methods.py
# Instance Methods

print("=== Instance Methods ===\n")

class Dog:
    def __init__(self, name, energy=100):
        self.name = name
        self.energy = energy
    
    def bark(self):
        print(f"{self.name} says: Woof! Woof!")
    
    def play(self):
        if self.energy >= 20:
            self.energy -= 20
            print(f"{self.name} plays! Energy: {self.energy}")
        else:
            print(f"{self.name} is too tired to play")
    
    def rest(self):
        self.energy = min(100, self.energy + 30)
        print(f"{self.name} rests. Energy: {self.energy}")
    
    def status(self):
        return f"{self.name}: {self.energy}/100 energy"

# Use methods
buddy = Dog("Buddy")
buddy.bark()
print(buddy.status())

print("\n=== Method Chaining State ===")

buddy.play()
buddy.play()
buddy.play()
buddy.play()  # Energy check
buddy.rest()
print(buddy.status())

print("\n=== Methods Returning Values ===")

class Calculator:
    def __init__(self, initial=0):
        self.value = initial
    
    def add(self, n):
        self.value += n
        return self.value
    
    def multiply(self, n):
        self.value *= n
        return self.value
    
    def reset(self):
        self.value = 0
        return self.value

calc = Calculator(10)
print(f"Start: {calc.value}")
print(f"Add 5: {calc.add(5)}")
print(f"Multiply by 3: {calc.multiply(3)}")
print(f"Current: {calc.value}")

print("\n=== Methods Calling Other Methods ===")

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        self._log_transaction("deposit", amount)
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            self._log_transaction("withdraw", amount)
            return True
        return False
    
    def _log_transaction(self, type, amount):
        print(f"[LOG] {self.owner}: {type} ${amount}, balance ${self.balance}")

account = BankAccount("Alice", 100)
account.deposit(50)
account.withdraw(30)

Methods take self as first parameter to access the object's attributes.

method Function defined inside a class. First parameter is `self`.

Multiple objects

Each object is independent with its own data.

multiple_objects.py
# Working with Multiple Objects

print("=== Multiple Objects ===\n")

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    def study(self, hours):
        improvement = hours * 2
        self.grade = min(100, self.grade + improvement)
    
    def display(self):
        print(f"{self.name}: {self.grade}/100")

# Create multiple students
alice = Student("Alice", 85)
bob = Student("Bob", 72)
carol = Student("Carol", 90)

# Each has own state
print("Initial grades:")
alice.display()
bob.display()
carol.display()

print("\n=== Objects Act Independently ===")

# Changes to one don't affect others
bob.study(5)  # Bob studies
carol.study(3)

print("\nAfter studying:")
alice.display()  # Unchanged
bob.display()    # Improved by 10
carol.display()  # Improved by 6

print("\n=== Objects in a List ===")

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def apply_discount(self, percent):
        self.price *= (1 - percent/100)

# Store objects in list
products = [
    Product("Laptop", 999),
    Product("Phone", 699),
    Product("Tablet", 499)
]

# Process all objects
print("Original prices:")
for p in products:
    print(f"  {p.name}: ${p.price}")

# Apply discount to all
for p in products:
    p.apply_discount(10)

print("\nAfter 10% discount:")
for p in products:
    print(f"  {p.name}: ${p.price:.2f}")

print("\n=== Objects Interacting ===")

class Player:
    def __init__(self, name, health=100):
        self.name = name
        self.health = health
    
    def attack(self, other, damage):
        other.health -= damage
        print(f"{self.name} attacks {other.name} for {damage} damage!")
        print(f"  {other.name}'s health: {other.health}")
    
    def is_alive(self):
        return self.health > 0

# Two players interact
hero = Player("Hero")
monster = Player("Dragon", 150)

hero.attack(monster, 30)
monster.attack(hero, 25)
hero.attack(monster, 40)

print(f"\nFinal: Hero={hero.health}, Dragon={monster.health}")

Changes to one object don't affect others. Each has its own memory.

Classes with business logic

Methods that compute and transform data.

class_with_logic.py
# Classes with Business Logic

print("=== Shopping Cart ===\n")

class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, name, price, quantity=1):
        item = {"name": name, "price": price, "qty": quantity}
        self.items.append(item)
        print(f"Added {quantity}x {name} @ ${price}")
    
    def remove_item(self, name):
        for item in self.items:
            if item["name"] == name:
                self.items.remove(item)
                print(f"Removed {name}")
                return True
        print(f"{name} not found")
        return False
    
    def get_total(self):
        total = 0
        for item in self.items:
            total += item["price"] * item["qty"]
        return total
    
    def display(self):
        print("\n--- Cart Contents ---")
        for item in self.items:
            subtotal = item["price"] * item["qty"]
            print(f"  {item['name']}: {item['qty']} x ${item['price']} = ${subtotal}")
        print(f"Total: ${self.get_total():.2f}")

# Use the cart
cart = ShoppingCart()
cart.add_item("Apple", 0.50, 5)
cart.add_item("Bread", 2.50)
cart.add_item("Milk", 3.00, 2)
cart.display()

cart.remove_item("Bread")
cart.display()

print("\n=== Temperature Converter ===")

class Temperature:
    def __init__(self, celsius=0):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    @property
    def kelvin(self):
        return self.celsius + 273.15
    
    def set_fahrenheit(self, f):
        self.celsius = (f - 32) * 5/9
    
    def describe(self):
        if self.celsius < 0:
            return "Freezing"
        elif self.celsius < 15:
            return "Cold"
        elif self.celsius < 25:
            return "Comfortable"
        else:
            return "Hot"

temp = Temperature(25)
print(f"{temp.celsius}°C = {temp.fahrenheit}°F = {temp.kelvin}K")
print(f"Feeling: {temp.describe()}")

temp.set_fahrenheit(32)  # Set from Fahrenheit
print(f"\n32°F = {temp.celsius:.1f}°C ({temp.describe()})")

print("\n=== Counter with History ===")

class Counter:
    def __init__(self, start=0):
        self.value = start
        self.history = [start]
    
    def increment(self, amount=1):
        self.value += amount
        self.history.append(self.value)
    
    def decrement(self, amount=1):
        self.value -= amount
        self.history.append(self.value)
    
    def undo(self):
        if len(self.history) > 1:
            self.history.pop()
            self.value = self.history[-1]

counter = Counter()
counter.increment(5)
counter.increment(3)
counter.decrement(2)
print(f"Value: {counter.value}")
print(f"History: {counter.history}")

counter.undo()
print(f"After undo: {counter.value}")
print(f"History: {counter.history}")

Classes can encapsulate complex logic, not just data storage.

Exercise: practical.py

Build a complete class with attributes, methods, and logic