← Previous

Lesson 20

Classes

~35 min

1. Classes in Python

Python is an object-oriented programming language at its core. While functions allow you to group reusable actions together, Classes allow you to group both data (attributes) and behaviors (methods) into a single, cohesive unit. Mastering classes allows you to build sophisticated, structured applications like web frameworks, game engines, and data models.

Learning Objectives

  • Understand the syntax for creating and instantiating custom Python classes.
  • Distinguish clearly between Instance Attributes and Class Attributes.
  • Master special "Dunder" (double underscore) methods like __init__() and __str__().
  • Implement custom instance methods to modify an object's internal state.
  • Follow Pythonic conventions like PascalCase naming for class definitions.

2. Understanding Classes: The Architectural Blueprint

A Class serves as a blueprint or template for creating objects. An Instance (or Object) is the actual physical item built using that blueprint.

Imagine a blueprint for a Smartphone. The blueprint defines that every phone will have a brand, a model, and a battery_level (attributes), and that every phone can make_call() or charge() (methods). However, your personal iPhone and your friend's Samsung Galaxy are two distinct instances created from that concept, each holding its own battery percentage and unique data.

Python code example:

# Defining a class using PascalCase convention
class Smartphone:
    # Class Attribute (shared across ALL instances)
    category = "Mobile Electronics"

    # Constructor method for setting Instance Attributes (unique per object)
    def __init__(self, brand, model, battery_level=100):
        self.brand = brand
        self.model = model
        self.battery_level = battery_level

# Instantiating (creating) two distinct objects
phone1 = Smartphone("Apple", "iPhone 15", 85)
phone2 = Smartphone("Samsung", "Galaxy S24", 92)

print(f"Phone 1: {phone1.brand} {phone1.model} ({phone1.battery_level}%)")
print(f"Phone 2: {phone2.brand} {phone2.model} ({phone2.battery_level}%)")

3. Instance Attributes vs. Class Attributes

Understanding where a variable lives inside a class is critical for avoiding unexpected bugs:

  • Instance Attributes: Variables attached to self inside __init__(). Every single object gets its own independent copy of these values.
  • Class Attributes: Variables defined directly inside the class block (outside any method). They are shared across every instance of that class to conserve memory.
Attribute Type Where Defined Scope & Ownership Best Used For
Instance Attribute Inside __init__() using self.attr Unique to each individual object. Data that varies per object (e.g., username, balance, ID).
Class Attribute Directly under class ClassName: Shared across all instances of the class. Constants, global counters, or shared configurations.

4. Special Dunder Methods: __init__ and __str__

Python classes feature special methods surrounded by double underscores, often referred to as Dunder Methods or magic methods:

1. The __init__() Method

The constructor method that Python calls automatically whenever a new instance is instantiated. It sets up the object's initial state.

2. The __str__() Method

Determines how the object is represented as a string when passed to print() or str(). Without __str__(), printing an object outputs an unhelpful memory address like <__main__.Smartphone object at 0x7f8a>.

Python code example:

class Laptop:
    def __init__(self, brand, ram_gb):
        self.brand = brand
        self.ram_gb = ram_gb

    # Custom string representation for human-readable output
    def __str__(self):
        return f"{self.brand} Laptop with {self.ram_gb}GB RAM"

my_laptop = Laptop("Dell", 16)

# Automatically triggers __str__()!
print(my_laptop)  # Output: Dell Laptop with 16GB RAM

5. Adding Custom Instance Methods

Methods are functions defined inside a class that operate on the instance's data. They can read or modify self attributes to update the object's state over time.

Python code example:

class BankAccount:
    def __init__(self, account_holder, balance=0.0):
        self.account_holder = account_holder
        self.balance = balance

    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            print(f"Deposited ${amount:.2f}. New Balance: ${self.balance:.2f}")

    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. Remaining Balance: ${self.balance:.2f}")
        else:
            print("Transaction declined: Insufficient funds or invalid amount.")

# Usage
account = BankAccount("Alex", 150.00)
account.deposit(50.00)    # Deposited $50.00. New Balance: $200.00
account.withdraw(75.00)   # Withdrew $75.00. Remaining Balance: $125.00

6. Real World Example: RPG Video Game Character

Classes shine when building game characters, UI elements, or database entities where each entity needs independent state tracking:

class Hero:
    # Class attribute to track total created heroes
    total_heroes = 0

    def __init__(self, name, hero_class, health=100):
        self.name = name
        self.hero_class = hero_class
        self.health = health
        self.inventory = []
        Hero.total_heroes += 1  # Increment global class counter

    def take_damage(self, damage):
        self.health = max(0, self.health - damage)
        print(f"🛡️ {self.name} took {damage} damage! Health is now {self.health}.")

    def heal(self, amount):
        self.health += amount
        print(f"✨ {self.name} healed for {amount} HP! Health is now {self.health}.")

    def add_item(self, item_name):
        self.inventory.append(item_name)
        print(f"🎒 {self.name} found: {item_name}")

# Simulating gameplay
knight = Hero("Arthur", "Paladin", health=120)
mage = Hero("Merlin", "Wizard", health=80)

knight.take_damage(30)
knight.add_item("Excalibur")
mage.heal(15)

print(f"Total active heroes in world: {Hero.total_heroes}")

7. Common Beginner Mistakes

⚠️ Watch out for these mistakes:

  • Forgetting self in Method Declarations: Writing def show_details(): inside a class raises TypeError: show_details() takes 0 positional arguments but 1 was given when called on an instance.
  • Mutating Mutable Class Attributes: Defining a list as a class attribute (e.g., items = [] outside __init__) shares that exact same list across ALL instances, leading to corrupted data!
  • Not Returning a String from __str__(): The __str__() method must return a string. Returning an integer or None will raise a TypeError.
  • Not Using PascalCase for Class Names: Naming classes in lower_snake_case (e.g., class bank_account:) violates Python PEP 8 conventions. Use class BankAccount: instead.

8. Code Examples

Here are production-ready patterns for defining and working with classes in Python:

# 1. Class tracking object counts with class attributes
class Employee:
    emp_count = 0

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
        Employee.emp_count += 1

    def __str__(self):
        return f"Employee: {self.name} | Salary: ${self.salary:,}"

e1 = Employee("Sarah", 85000)
e2 = Employee("David", 92000)

print(e1)
print(f"Total Employees: {Employee.emp_count}")

# 2. Encapsulating state modifications cleanly
class Thermostat:
    def __init__(self, temp_celsius=20.0):
        self.temp_celsius = temp_celsius

    def set_temp(self, new_temp):
        if -10.0 <= new_temp <= 45.0:
            self.temp_celsius = new_temp
        else:
            print("Temperature out of safe operating range!")
Interactive Practice

Mini Practice: Car Class Builder

Create a class named Car with an __init__() method that initializes make, model, and speed (default 0). Add an accelerate(amount) method that increases speed and a __str__() method that returns "Make Model running at Speed km/h".

0% complete
Check when done
Quiz

Test Your Understanding

1. What is the difference between a Class and an Instance (Object)?

2. What happens when you print an object that has a custom `__str__()` method defined?

3. What is the scope of a Class Attribute defined directly under `class Student:`?

4. According to PEP 8 naming conventions, how should Python classes be named?

9. Challenge Project

Student Grade Tracker: Build a Student class that tracks academic performance:

  1. Include attributes for name, student_id, and a list of grades (starts empty).
  2. Add an add_grade(score) method that appends valid scores (0-100) to the list.
  3. Add a get_average() method that calculates and returns the student's average test score.
  4. Implement a __str__() method that displays the student name, ID, and average score rounded to 1 decimal place!

10. Key Takeaways

  • Classes group state (attributes) and behavior (methods) into reusable blueprints.
  • Instantiate objects by calling the class name like a function: obj = ClassName().
  • Use __init__() to define instance attributes unique to each object.
  • Implement __str__() to provide clean, human-readable string representations.
  • Instance attributes belong to specific objects; Class attributes are shared across all instances.

🚀 Progression

You've mastered Python Classes! Next up, we will explore code reuse and hierarchy with **Inheritance & Polymorphism in Python**!