Python Classes · Object-Oriented Programming

Mingming Li — Click any blue heading below to expand the content.

Table of Contents

1. What is a class? (Blueprint for objects)

A class is a blueprint for creating objects. It defines the properties (attributes) and behaviors (methods) that objects of that class will have. Object-Oriented Programming (OOP) helps organize code by modeling real-world entities.

Think of a class like a cookie cutter, and objects are the cookies. One blueprint can create many objects!
# Defining a simple class
class Dog:
    def __init__(self, name, age):
        self.name = name   # attribute
        self.age = age     # attribute
    
    def bark(self):        # method
        print(f"{self.name} says: Woof!")
    
    def get_info(self):
        return f"{self.name} is {self.age} years old"

# Creating objects (instances) of the class
my_dog = Dog("Buddy", 3)
your_dog = Dog("Max", 5)

# Accessing attributes and methods
print(my_dog.name)        # Buddy
print(my_dog.get_info())  # Buddy is 3 years old
my_dog.bark()             # Buddy says: Woof!
Key concepts: __init__ is the constructor (called when creating an object). self refers to the current instance (like saying "this object").

2. The __init__ method (constructor)

The __init__ method runs automatically when you create a new object. It sets up the initial state of the object.

class Student:
    def __init__(self, name, grade, student_id):
        self.name = name
        self.grade = grade
        self.student_id = student_id
        self.attendance = 0    # default value (not passed as parameter)
    
    def record_attendance(self):
        self.attendance += 1
        print(f"{self.name} has attended {self.attendance} day(s)")
    
    def display_info(self):
        return f"ID: {self.student_id}, Name: {self.name}, Grade: {self.grade}"

# Creating objects
student1 = Student("Alice", "A", "S001")
student2 = Student("Bob", "B", "S002")

print(student1.display_info())   # ID: S001, Name: Alice, Grade: A
student1.record_attendance()     # Alice has attended 1 day(s)
student1.record_attendance()     # Alice has attended 2 day(s)
Note: self.attendance = 0 is a default value. Not every attribute needs to come from a parameter!

3. Instance methods

Instance methods are functions inside a class that operate on the object's data. They always take self as the first parameter.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
    
    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            print(f"Deposited ${amount}. New balance: ${self.balance}")
        else:
            print("Deposit amount must be positive")
    
    def withdraw(self, amount):
        if amount > 0 and amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.balance}")
        else:
            print("Insufficient funds or invalid amount")
    
    def get_balance(self):
        return f"{self.owner}'s balance: ${self.balance}"

# Using the class
account = BankAccount("Mingming", 100)
account.deposit(50)      # Deposited $50. New balance: $150
account.withdraw(30)     # Withdrew $30. New balance: $120
print(account.get_balance())  # Mingming's balance: $120

4. Class attributes vs instance attributes

Class attributes are shared by ALL objects. Instance attributes are unique to EACH object.
class Employee:
    # Class attribute (shared by all instances)
    company = "Tech Corp"
    employee_count = 0
    
    def __init__(self, name, salary):
        # Instance attributes (unique to each instance)
        self.name = name
        self.salary = salary
        Employee.employee_count += 1  # increment class attribute
    
    def display_info(self):
        return f"{self.name} works at {Employee.company}, salary: ${self.salary}"

# Creating instances
emp1 = Employee("Alice", 50000)
emp2 = Employee("Bob", 60000)

print(emp1.display_info())  # Alice works at Tech Corp, salary: $50000
print(emp2.display_info())  # Bob works at Tech Corp, salary: $60000
print(f"Total employees: {Employee.employee_count}")  # Total employees: 2

# Modifying class attribute affects all instances
Employee.company = "New Tech Inc"
print(emp1.display_info())  # Alice works at New Tech Inc, salary: $50000

5. Inheritance (reusing code)

Inheritance lets a child class reuse code from a parent class. This is great for avoiding duplication!

# Parent class (base class)
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        print(f"{self.name} makes a sound")
    
    def move(self):
        print(f"{self.name} moves")

# Child class (subclass) inherits from Animal
class Dog(Animal):
    def speak(self):   # Override parent method
        print(f"{self.name} barks: Woof!")
    
    def fetch(self):   # New method specific to Dog
        print(f"{self.name} fetches the ball")

class Cat(Animal):
    def speak(self):   # Override parent method
        print(f"{self.name} meows: Meow!")
    
    def climb(self):   # New method specific to Cat
        print(f"{self.name} climbs a tree")

# Using inheritance
animals = [Dog("Buddy"), Cat("Whiskers"), Animal("Generic")]
for animal in animals:
    animal.speak()
    animal.move()
    print()

# Calling subclass-specific methods
buddy = Dog("Buddy")
buddy.fetch()   # Buddy fetches the ball

6. The super() function

super() lets you call methods from the parent class. This is useful when you want to extend (not replace) parent behavior.

class Vehicle:
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year
    
    def start(self):
        print(f"{self.brand} {self.model} is starting")
    
    def info(self):
        return f"{self.year} {self.brand} {self.model}"

class Car(Vehicle):
    def __init__(self, brand, model, year, doors):
        super().__init__(brand, model, year)  # Call parent constructor
        self.doors = doors
    
    def start(self):
        super().start()  # Call parent method
        print("Vroom vroom! The car is ready to go")
    
    def info(self):
        return f"{super().info()} with {self.doors} doors"

# Using super()
car = Car("Toyota", "Camry", 2022, 4)
print(car.info())   # 2022 Toyota Camry with 4 doors
car.start()         # Toyota Camry is starting
                    # Vroom vroom! The car is ready to go

7. Special methods (__str__, __len__, etc.)

Special methods (also called "dunder methods" because they have double underscores) let your objects work with built-in Python functions like print(), len(), and ==.

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):   # Called by print() and str()
        return f"'{self.title}' by {self.author}"
    
    def __repr__(self):  # Representation for developers
        return f"Book('{self.title}', '{self.author}', {self.pages})"
    
    def __len__(self):   # Called by len()
        return self.pages
    
    def __eq__(self, other):  # Called by ==
        if isinstance(other, Book):
            return self.title == other.title and self.author == other.author
        return False
    
    def __lt__(self, other):  # Called by < (for sorting)
        return self.pages < other.pages

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

print(str(book1))   # 'Python Crash Course' by Eric Matthes
print(len(book1))   # 544
print(book1 < book2)   # True (544 < 592)

books = [book2, book1]
books.sort()   # Sorts by pages (using __lt__)
print(books[0])  # 'Python Crash Course' by Eric Matthes
Common special methods: __init__, __str__, __repr__, __len__, __eq__, __lt__, __add__, etc.

8. Encapsulation (private attributes)

Private attributes (with double underscore __) cannot be accessed directly from outside the class. This protects sensitive data.

class BankAccount:
    def __init__(self, account_number, initial_balance=0):
        self.account_number = account_number
        self.__balance = initial_balance  # Private attribute (double underscore)
    
    def get_balance(self):
        """Access private balance through a method"""
        return self.__balance
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"Deposited ${amount}. New balance: ${self.__balance}")
        else:
            print("Deposit amount must be positive")
    
    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.__balance}")
        else:
            print("Insufficient funds or invalid amount")
    
    @property
    def balance(self):
        return self.__balance

account = BankAccount("ACC123", 100)
# print(account.__balance)  # Error! Cannot access private attribute directly
print(account.get_balance())  # 100
print(account.balance)        # 100 (using property)

9. Quick reference

# DEFINING A CLASS
class ClassName:
    class_attribute = "shared"           # class attribute
    
    def __init__(self, param1, param2):  # constructor
        self.instance_attr = param1      # instance attribute
        self.__private = "secret"        # private (cannot access outside)
    
    def instance_method(self):           # instance method
        return self.instance_attr

# CREATING OBJECTS
obj = ClassName(value1, value2)

# INHERITANCE
class ChildClass(ParentClass):
    def __init__(self, param1, param2, extra):
        super().__init__(param1, param2)  # call parent constructor
        self.extra = extra

# COMMON SPECIAL METHODS
__init__(self)      # constructor
__str__(self)       # called by print()
__len__(self)       # called by len()
__eq__(self, other) # called by ==
__lt__(self, other) # called by <

10. Quick Challenge: Spot the Class Parts

Look at this code from our game (Code-3 could use classes!). Imagine we create a Cactus class:

class Cactus:
    def __init__(self, x, y, image):
        self.x = x
        self.y = y
        self.image = image
        self.rect = self.image.get_rect(midbottom=(x, y))
    
    def move(self, speed):
        self.x -= speed
        self.rect.x = self.x
    
    def draw(self, screen):
        screen.blit(self.image, self.rect)

Questions:

  1. What is the name of the class?
  2. What are the attributes (data stored in each cactus)?
  3. What are the methods (behaviors)?
  4. What does self refer to?
Click for answers
  1. Cactus
  2. x, y, image, rect
  3. __init__ (constructor), move(), draw()
  4. self refers to the specific cactus object being created or used

11. Exercises

Try these exercises to practice classes. Write your code in Thonny.

  • Exercise 1: Create a class Rectangle with width and height. Add area() and perimeter() methods.
  • Exercise 2: Create a class Circle with radius. Add area() and circumference() (use 3.14 for pi).
  • Exercise 3: Create a class Person with name and age. Add greet() that prints "Hello, my name is [name]".
  • Exercise 4: Create a class BankAccount with owner and balance. Add deposit(), withdraw(), and display_balance().
  • Exercise 5: Create a class Student with name and grades list. Add add_grade() and average().
  • Exercise 6: Create a class Book. Override __str__() to return a formatted string.
  • Exercise 7: Use inheritance: Create Vehicle parent class and Car and Bicycle child classes.
  • Exercise 8: Create a class Counter that counts how many objects have been created (use a class attribute).

12. Common Errors & Solutions

  • TypeError: __init__() missing 1 required positional argument
    -> You forgot to pass all required parameters when creating an object.
  • NameError: name 'self' is not defined
    -> You forgot to include self as the first parameter of a method.
  • AttributeError: 'Class' object has no attribute 'xxx'
    -> You misspelled an attribute name or forgot to define it in __init__.
  • RecursionError (with inheritance)
    -> You accidentally made a class inherit from itself!
Debugging tip: Add print() statements inside methods to see what's happening. Use print(type(obj)) to check object types.

13. Check Your Understanding

  1. What is the difference between a class and an object?
  2. What does self represent?
  3. When is the __init__ method called?
  4. What is inheritance?
  5. How do you make an attribute private?
Click for Answers
  1. A class is a blueprint; an object is an instance created from that blueprint.
  2. self refers to the current instance of the class.
  3. __init__ is called automatically when you create a new object.
  4. Inheritance lets a child class reuse code from a parent class.
  5. Use double underscore prefix: __private_attribute

14. Your Progress Tracker

Check off each item as you master it:
I understand what a class is and how to define one
I understand the __init__ constructor method
I can create objects from a class
I understand the difference between class and instance attributes
I can use inheritance to create child classes
I can use super() to call parent methods
I understand special methods like __str__ and __len__
I completed at least 5 exercises
I can spot classes in our game code