12. Intro to Classes#

In this notebook, we cover the following subjects:

  • What is a class,

  • The __init__() method,

  • The __str__() method


# To enable type hints for lists, dicts, and tuples, we need to import the following:
from typing import List, Dict, Tuple

In this notebook, we’re diving into something that will take your programming skills to the next level: Object-Oriented Programming (OOP) with classes!

Object-Oriented Programming (OOP) is a programming paradigm that helps you structure your code by grouping related properties and behaviors into individual objects. OOP makes it easier to model real-world things in code, reuse components, and manage complexity. In Python, classes are the foundation of OOP, allowing you to create your own custom objects that have both data (attributes) and behaviors (methods).

You’ve already learned so much about Python—functions, variables, loops, and data structures. Now, we’re going to bring all of that knowledge together in a new and exciting way. Classes are a fundamental part of Object-Oriented Programming (OOP) and will help you organize your code, making it easier to work with and more powerful. Think of classes as blueprints. Just like how a blueprint of a house lets you create many houses, a class in Python lets you create many similar objects.

Let’s start by understanding what a class is.

12.1. What is a class#

A class is like a template or a recipe for creating things in Python. These “things” are called objects. Imagine you want to describe a cat. You could describe it by its color, age, name, and personality. Instead of describing each cat over and over again, you can create a class called Cat that defines these attributes, and then use it to make individual cats. Let’s see this in action!

12.1.1. Defining a class#

Here, we’ll define a class called Cat.

class Cat:
    pass

This is the most basic form of a class. The keyword class is used to create it, and Cat is the name we chose. The keyword pass just means “do nothing” for now. This is a placeholder.

But… a class without any features isn’t very interesting, right? Let’s make it more useful!

12.1.2. The __init__() Method#

class Cat:
    """
    A class representing a cat.

    Attributes:
        name (str): The name of the cat.
        age (int): The age of the cat in years.
    """
    def __init__(self, name: str, age: int) -> None:
        """
        Initializes a new instance of the Cat class.

        Parameters:
            name (str): The name of the cat.
            age (int): The age of the cat in years.
        """
        self.name = name  # This assigns the name to the cat (attribute)
        self.age = age    # This assigns the age to the cat (attribute)

Here’s what’s happening:

  • __init__() is a special method called a constructor.

  • self is like the cat itself. It’s a reference to the object we are creating.

  • name and age are the things we want to store about each cat (attributes of a cat).

  • self.name = name stores the name given to the Cat object (same with the age).

Let’s make our first cat!

class Cat:
    """
    A class representing a cat.

    Attributes:
        name (str): The name of the cat.
        age (int): The age of the cat in years.
    """
    def __init__(self, name: str, age: int) -> None:
        """
        Initializes a new instance of the Cat class.

        Parameters:
            name (str): The name of the cat.
            age (int): The age of the cat in years.
        """
        self.name = name
        self.age = age


my_cat: Cat = Cat("Whiskers", 3)
print(my_cat)
<__main__.Cat object at 0x7f9ecc4fdee0>

This looks a bit weird, right? That’s because Python doesn’t know how to nicely print out your Cat object yet. By default, it just shows you the class name (Cat) and where it’s stored in memory (0x000002630A190A10).

Let’s check out two ways to print it nicely. For instance, we could do something like this:

print(f"My cat's name is {my_cat.name} and it is {my_cat.age} years old.")
My cat's name is Whiskers and it is 3 years old.

Way better right? But what’s happening here?

Instead of printing the whole object (my_cat) directly, we used its attributes (name and age) to create a friendly sentence.

my_cat.name is accessing the name we gave our cat (which is “Whiskers”). my_cat.age is getting the age we set (which is 3).

But why does this work?

In Python, every time we use the dot ('.') (like my_cat.name), you’re telling Python to reach into your Cat object and get a specific piece of information. This is called accessing an attribute.

So, while printing the object (my_cat) by itself looks weird because Python doesn’t know how to display it nicely, using its attributes gives you more control over what gets printed.

Remember I told you will will check 2 different ways to print our cat nicely? Well, I do! So here is the 2nd option.

Note

When you define a new class, make sure that the name of the class (like Cat) starts with a capital letter (C in this case).

12.1.3. The __str__() Method#

Instead of printing the attributes manually every time, we can “teach” Python how to print our Cat objects automatically by adding a __str__() method. The __str__() method is a special method that tells Python how to represent an object as a string. By default, if you print an object without defining __str__(), you get a message like <__main__.Cat object at 0x000002>, which is not very informative (as we have seen). The __str__() method allows you to define a meaningful, human-readable string that describes the object in a clear way.

Here’s how it works:

  • __str__() is called automatically when you use the print() function on an object that has a __str__() defined, or when you try to convert an object to a string using str().

  • Inside the __str__() method, you need to return a string that represents the object in the way you want it to appear.

Let’s add it to our Cat class to see how it improves the output.

class Cat:
    """
    A class representing a cat.

    Attributes:
        name (str): The name of the cat.
        age (int): The age of the cat in years.
    """
    def __init__(self, name: str, age: int) -> None:
        """
        Initializes a new instance of the Cat class.

        Parameters:
            name (str): The name of the cat.
            age (int): The age of the cat in years.
        """
        self.name = name
        self.age = age

    def __str__(self) -> str:
        """
        Returns a string representation of the Cat instance.

        Returns:
            str: A description of the cat including its name and age.
        """
        return f"Cat named {self.name} who is {self.age} years old."

Now, when we print my_cat, it will look much nicer:

my_cat = Cat("Whiskers", 3)
print(my_cat)
Cat named Whiskers who is 3 years old.

Note

Remember, you need to add typehints and docstrings to classes as well. This means that bellow the class definition there should be a docstring (see code above) and all other methods inside the class must have both typehints and a docstring.

12.1.4. A few more example classes#

In this example, we’ll define a Panda class. We’ll use the __str__() method to format a readable message about the panda, including its favorite food and activity.

class Panda:
    """
    A class to represent a panda.
    
    Attributes:
        name (str): The name of the panda.
        age (int): The age of the panda.
        favorite_food (str): The panda's favorite food.
    """
    def __init__(self, name: str, age: int, favorite_food: str) -> None:
        """
        Initializes a new instance of the Panda class.

        Parameters:
            name (str): The name of the panda.
            age (int): The age of the panda.
            favorite_food (str): The favorite food of the panda.
        """
        self.name = name
        self.age = age
        self.favorite_food = favorite_food

    def __str__(self) -> str:
        """
        Returns a string representation of the Panda instance.

        Returns:
            str: A description of the panda including its name, age, and favorite food.
        """
        return f"{self.name} is a {self.age}-year-old panda who loves eating {self.favorite_food}."

# Creating an instance of Panda
panda1 = Panda("Bao Bao", 5, "bamboo shoots")
print(panda1)
Bao Bao is a 5-year-old panda who loves eating bamboo shoots.

Explanation:

We created a Panda class with attributes:

  • name

  • age

  • and favorite_food.

The __str__() method returns a sentence that includes all these details.

Let’s see one more example:

Now let’s create a Jungle class. This one will be a bit different: it will describe the jungle by listing the types of animals and the number of trees in it. We’ll use the __str__() method to format this information in a more structured way.

class Jungle:
    """
    A class to represent a jungle.
    
    Attributes:
        name (str): The name of the jungle.
        animals (list[str]): A list of animal species found in the jungle.
        tree_count (int): The number of trees in the jungle.
    """
    def __init__(self, name: str, animals: list, tree_count: int) -> None:
        """
        Initializes a new instance of the Jungle class.

        Parameters:
            name (str): The name of the jungle.
            animals (list[str]): A list of the names of animal species found in the jungle.
            tree_count (int): The number of trees in the jungle.
        """
        self.name = name
        self.animals = animals
        self.tree_count = tree_count

    def __str__(self) -> str:
        """
        Returns a string representation of the Jungle instance, providing a welcome message and details about the jungle.

        Returns:
            str: A detailed description of the jungle including its name, animals, and tree count.
        """
        animal_list = ", ".join(self.animals)
        return (f"Welcome to the {self.name} Jungle!\n"
                f"Animals here: {animal_list}.\n"
                f"Number of trees: {self.tree_count}.")

# Creating an instance of Jungle
jungle1 = Jungle("Amazon", ["tigers", "monkeys", "parrots"], 15000)
print(jungle1)
Welcome to the Amazon Jungle!
Animals here: tigers, monkeys, parrots.
Number of trees: 15000.

Explanation:

This Jungle class includes attributes:

  • name

  • animals (a list)

  • and tree_count

The __str__() method combines the attributes into a multi-line string for a nice, clear description.

That’s it about classes for now. Let’s Practice!

12.2. Exercises#

Let’s practice! Mind that each exercise is designed with multiple levels to help you progressively build your skills. Level 1 is the foundational level, designed to be straightforward so that everyone can successfully complete it. In Level 2, we step it up a notch, expecting you to use more complex concepts or combine them in new ways. Finally, in Level 3, we get closest to exam level questions, but we may use some concepts that are not covered in this notebook. However, in programming, you often encounter situations where you’re unsure how to proceed. Fortunately, you can often solve these problems by starting to work on them and figuring things out as you go. Practicing this skill is extremely helpful, so we highly recommend completing these exercises.

For each of the exercises, make sure to add a docstring and type hints, and do not import any libraries unless specified otherwise.

12.2.1. Exercise 1#

Level 1: Create a Book class with attributes for title, author, and year. Implement the __str__() method to print the book details in a readable format. Example input:

book1: Book = Book("To Kill a Mockingbird", "Harper Lee", 1960)

Example output:

"To Kill a Mockingbird" by Harper Lee (1960)
# TODO.

Level 2: Extend your class to handle a list of books. Create a list of Book objects and write a function that loops over theese objects and prints out the details of each book.

Example input:

books: List[Book] = [
    Book("1984", "George Orwell", 1949),
    Book("Pride and Prejudice", "Jane Austen", 1813),
    Book("The Great Gatsby", "F. Scott Fitzgerald", 1925)
]

Example output:

"1984" by George Orwell (1949)
"Pride and Prejudice" by Jane Austen (1813)
"The Great Gatsby" by F. Scott Fitzgerald (1925)

# TODO.

Level 3: Add a feature to your Book class to store a list of keywords for each book. Modify the __str__() method to include these keywords if they are available. If no keyword is available, add None.

Example input:

book2: Book = Book("Brave New World", "Aldous Huxley", 1932, ["dystopia", "science fiction", "totalitarianism"])

Example output:

"Brave New World" by Aldous Huxley (1932) - Keywords: dystopia, science fiction, totalitarianism

# TODO.

12.2.2. Exercise 2#

Level 1: Create a Recipe class with attributes for name, ingredients (a list), and time (in minutes). Implement the __str__() method to print the recipe details in a readable format.

Example input:

recipe1: Recipe = Recipe("Pasta Carbonara", ["pasta", "eggs", "cheese", "bacon"], 20)

Example output:

Pasta Carbonara (20 mins) - Ingredients: pasta, eggs, cheese, bacon

# TODO.

Level 2: Write a program that takes a list of Recipe objects and prints only the recipes that can be prepared in less than 30 minutes.

Example input:

recipes: List[Recipe] = [
    Recipe("Salad", ["lettuce", "tomatoes", "cucumber"], 10),
    Recipe("Lasagna", ["pasta", "cheese", "tomato sauce", "ground beef"], 60),
    Recipe("Omelette", ["eggs", "cheese", "ham"], 15)
]

Example output:

Salad (10 mins) - Ingredients: lettuce, tomatoes, cucumber
Omelette (15 mins) - Ingredients: eggs, cheese, ham

# TODO.

Level 3: Enhance the Recipe class to include a difficulty level (“Easy”, “Medium”, “Hard”) based on the number of ingredients:

  • less than 4 ingredients is “Easy”

  • 4-6 is “Medium”

  • and more than 6 is “Hard”.

Update the __str__() method to include this information.

Example input:

recipe2: Recipe = Recipe("Chocolate Cake", ["flour", "sugar", "eggs", "chocolate", "butter", "milk"], 45)


Example output:

Chocolate Cake (45 mins) - Ingredients: flour, sugar, eggs, chocolate, butter, milk (Difficulty: Medium)

# TODO.

12.2.3. Exercise 3#

Level 1: Create a Movie class with attributes for title, director, and actors (a list). Implement the __str__() method to print the movie details in a readable format.

Example input:

movie1: Movie = Movie("Inception", "Christopher Nolan", ["Leonardo DiCaprio", "Joseph Gordon-Levitt"])

Example output:

"Inception" directed by Christopher Nolan. Starring: Leonardo DiCaprio, Joseph Gordon-Levitt

# TODO.

Level 2: Create a list of Movie objects and make a function called filter_by_director() that takes as input a director (str) and filters out only the movies which were directed by that director. Print the filtered movies.

Example input:

movies: List[Movie] = [
    Movie("Interstellar", "Christopher Nolan", ["Matthew McConaughey", "Anne Hathaway"]),
    Movie("Pulp Fiction", "Quentin Tarantino", ["John Travolta", "Uma Thurman"]),
    Movie("The Dark Knight", "Christopher Nolan", ["Christian Bale", "Heath Ledger"])
]

director: str = 'Christopher Nolan'

Example output:

"Interstellar directed by Christopher Nolan. Starring: Matthew McConaughey, Anne Hathaway"
"The Dark Knight" directed by Christopher Nolan. Starring: Christian Bale, Heath Ledger"

# TODO.

Level 3: Expand your Movie class to include more attributes and functionality. You’ll be handling multiple movies and performing advanced filtering and grouping operations.

Expand the Movie class to include the following attributes:

  • title (str): The name of the movie.

  • director (str): The director of the movie.

  • actors (list of str): A list of the main actors.

  • release_year (int): The year the movie was released.

  • genre (str): The genre of the movie.

  • ratings (list of float): A list of ratings from different critics (values between 1 and 10).

Implement a __str__() method to return a formatted description of the movie, including the average rating (rounded to 2 decimal places).

Then create a list of Movie objects and perform the following tasks (make functions for all):

  • Filter movies based on genre and average rating (greater than a given threshold).

  • Group the movies by director, and print out each director’s filmography. (think of an appropriate data type)

  • Find the highest-rated movie for a given genre (based on average rating).

  • Calculate the average rating of all movies released in a specific decade (e.g., 1990s, 2000s).

Example input:

movies: List[Movies] = [
    Movie("Inception", "Christopher Nolan", ["Leonardo DiCaprio", "Joseph Gordon-Levitt"], 2010, "Sci-Fi", [9.2, 8.9, 9.5]),
    Movie("Pulp Fiction", "Quentin Tarantino", ["John Travolta", "Uma Thurman"], 1994, "Crime", [9.0, 8.8, 9.3]),
    Movie("The Dark Knight", "Christopher Nolan", ["Christian Bale", "Heath Ledger"], 2008, "Action", [9.7, 9.4, 9.8]),
    Movie("The Matrix", "The Wachowskis", ["Keanu Reeves", "Laurence Fishburne"], 1999, "Sci-Fi", [8.7, 8.5, 8.9]),
    Movie("Interstellar", "Christopher Nolan", ["Matthew McConaughey", "Anne Hathaway"], 2014, "Sci-Fi", [9.1, 9.3, 9.2]),
    Movie("Kill Bill", "Quentin Tarantino", ["Uma Thurman"], 2003, "Action", [8.3, 8.0, 8.5])
]

Expected Output:

# Print the average rating for each movie:
"Inception (2010) directed by Christopher Nolan. Avg. Rating: 9.2"
"Pulp Fiction (1994) directed by Quentin Tarantino. Avg. Rating: 9.03"
"The Dark Knight (2008) directed by Christopher Nolan. Avg. Rating: 9.63"
"The Matrix (1999) directed by The Wachowskis. Avg. Rating: 8.7"
"Interstellar (2014) directed by Christopher Nolan. Avg. Rating: 9.2"
"Kill Bill (2003) directed by Quentin Tarantino. Avg. Rating: 8.27"


# Filter Sci-Fi movies with an average rating > 9.0:
"Interstellar (2014) directed by Christopher Nolan. Starring: Matthew McConaughey, Anne Hathaway (Genre: Sci-Fi) - Avg. Rating: 9.20"

# Group movies by director:
{
    'Christopher Nolan': [('Inception', 2010), ('The Dark Knight', 2008), ('Interstellar', 2014)]
    'Quentin Tarantino': ['Pulp Fiction', 1994), ('Kill Bill', 2003)]
    'The Wachowskis': [('The Matrix', 1999)]
}

# Find the highest-rated movie in the "Sci-Fi" genre:
"Highest-rated Sci-Fi movie: Interstellar with an average rating of 9.20"

# Average rating of movies released in the 2000s:
"Average rating for movies in the 2000s: 8.93"
# TODO.