Table of contents
Text Link

Exploring Python Magic Method Operators

Magic methods, also known as Dunder methods, are special Python methods that start and end with two underscores. They reference particular behaviors for specific actions or operations. Magic methods are automatically invoked when certain operations (such as arithmetic or comparison operators) are performed on class instances.

One of the critical benefits of magic methods is their ability to enable operator overloading. Operator overloading is the ability to reference multiple methods with the same name but different parameters. This can be used to extend existing operators' functionality and create more intuitive and expressive code.

This article will discuss Python magic methods for building a shopping cart class object. We will use magic methods to define custom behaviors for the following operations:

  • Initializing a cart
  • Adding items to a cart
  • Removing items from a cart
  • Calculating the total price of items in a cart
  • Viewing the list of items in a cart
  • Adding carts together

Construct, initialize, and destroy the ShoppingCart

These basic operators initialize, construct, or destroy objects in Python code. They are automatically invoked when an object is created or destroyed. The `__new__` method creates a new class iterable, the `__init__` method is used to initialize it, and the `__del__` method is used to destroy it.

Let's see how they are used to build the ShoppingCart class:

class ShoppingCart(object):
    
    __no_carts = 0
    
    def __init__(self):
        print("Initializing the ShoppingCart")
        self.items = []
        ShoppingCart.__no_carts += 1
        
    @classmethod
    def get_cart_number(cls):
        return cls.__no_carts

Let's create an instance of the class and see how this works:

cart1 = ShoppingCart()
print(f"The ShoppingCart Number is: {cart1.get_cart_number()}")
Initializing the ShoppingCart
The ShoppingCart Number is: 1

We created the `ShoppingCart` custom class in the above code snippet. When an instance of this custom class is created, the items list and cart number are initialized in the `__init__` magic method.

The class instance was first created before the initialization with the `__init__` method. It is implicit. We can, however, make the step explicit with the `__new__` method:

class ShoppingCart(object):
    
    __no_carts = 0
    
    def __new__(cls):
        print("Creating a new instance of the ShoppingCart")
        return object.__new__(cls)
    
    def __init__(self):
        print("Initializing the ShoppingCart")
        self.items = []
        ShoppingCart.__no_carts += 1
        
    @classmethod
    def get_cart_number(cls):
        return cls.__no_carts
cart1 = ShoppingCart()
print(f"The ShoppingCart Number is: {cart1.get_cart_number()}")
Creating a new instance of the Shopping
CartInitializing the ShoppingCart
The ShoppingCart Number is:1

When we introduced the __new__ method, we noticed when a new class instance was created. The __new__ method then calls the __init__ method to initialize the created ShoppingCart.

After creating an instance of the class, we may want to delete it. We can do so with the del keyword:

del cart1

If we try to call `cart1`, we get a `NameError` with the message name `cart1` that is not defined. We can check to see if the number of carts has decreased by 1:

ShoppingCart.get_cart_number()
1

We can see that we still maintain the same number of carts despite deleting `cart1`. The `del` keyword implicitly calls the `__del__` magic method to delete `cart1`. We can customize this operation also to decrease the number of carts for every delete operation:

class ShoppingCart(object):
    
    __no_carts = 0
    
    def __init__(self):
        print("Initializing the ShoppingCart")
        self.items = []
        ShoppingCart.__no_carts += 1
        
    def __del__(self):
        print(f"Deleting ShoppingCart Number {self.get_cart_number()}")
        if ShoppingCart.__no_carts != 0:
            ShoppingCart.__no_carts -= 1
        else:
            print("Not Implemented")
        
    @classmethod
    def get_cart_number(cls):
        return cls.__no_carts
cart1 = ShoppingCart()
print(f"The ShoppingCart Number is: {cart1.get_cart_number()}")
del cart1
Initializing the ShoppingCart
The ShoppingCart Number is: 1
Deleting ShoppingCart Number1

Now the number of carts should decrease by 1 when a cart is deleted:

ShoppingCart.get_cart_number()
0

Add and remove items from the ShoppingCart

The `add_item` and `remove_item` methods are used to add and remove items from the cart:

class ShoppingCart(object):
    
    __no_carts = 0
    
    def __init__(self):
        print("Initializing the ShoppingCart")
        self.items = []
        ShoppingCart.__no_carts += 1
        
    def __del__(self):
        print(f"Deleting ShoppingCart Number {self.get_cart_number()}")
        if ShoppingCart.__no_carts != 0:
            ShoppingCart.__no_carts -= 1
        else:
            print("Not Implemented")
        
    @classmethod
    def get_cart_number(cls):
        return cls.__no_carts
    
    def add_item(self, item):
        self.items.append(item)
        print(f"{item} has been added")
        
    def remove_item(self, item):
        if item in self.items:
            self.items.remove(item)
            print(f"{item} has been removed")
        else:
            print(f"{item} not found in cart")

An item has a `name` and `price`. To be able to add an item, we will create the `Item` class with these attributes:

class Item(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price

Suppose we are buying two shirts, a pair of jeans and shoes. Let's create and add these items to the cart:

shirt1 = Item("Shirt", 20)
shirt2 = Item("Shirt", 20)
shoe = Item("Shoe", 50)
jeans = Item("Jeans", 25)
cart1 = ShoppingCart()

cart1.add_item(shirt1)
cart1.add_item(shirt2)
cart1.add_item(shoe)
cart1.add_item(jeans)
Initializing the ShoppingCart
<__main__.Item object at 0x000001D86BF43580> has been added
<__main__.Item object at 0x000001D86BF43AC0> has been added
<__main__.Item object at 0x000001D86BF43520> has been added
<__main__.Item object at 0x000001D86BF43EE0> has been added

We realize that we may not require two shirts. So we want to remove one of them from the cart:

# Remove an item from the cart
cart1.remove_item(shirt2)
<__main__.Item object at 0x000001D86BF43AC0> has been removed

Wait a minute; the items we added and removed were not printed in a human-readable format. Why so? The next section will see how to print using Python magic methods.

Print from the Item and ShoppingCart class objects

When we tried to print the items added to `cart1`, we got the memory address of these items. It is difficult to read and understand what was printed. The `__repr__` and `__str__` methods can help us with strings. The `__repr__` method is used to return a string representation that is intended for programmers. In contrast, the `__str__` method returns a string representation designed for users.

The `__repr__` method returns a string representation of an object that can be used to recreate the object. It means the string should be unambiguous and contain all the information necessary to reconstruct the object. The `__str__` method, on the other hand, can return a more human-readable string representation of an object. This string does not need to be unambiguous or contain all the information about the object. It can be used to display the object to a user.

We can now move forward to process the items in the list; we will use the string representation `__repr__` method:

class Item(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price
        
    def __repr__(self):
        return f"Item('{self.name}', {self.price})"

Let's delete `cart1` and confirm that we no longer have any cart instances:

del cart1
ShoppingCart.get_cart_number()
Deleting ShoppingCart Number 1
0

Next, let's create a new cart and add the items to it:

shirt1 = Item("Shirt", 20)
shirt2 = Item("Shirt", 20)
shoe = Item("Shoe", 50)
jeans = Item("Jeans", 25)

cart1 = ShoppingCart()

cart1.add_item(shirt1)
cart1.add_item(shirt2)
cart1.add_item(shoe)
cart1.add_item(jeans)
Initializing the ShoppingCart
Item('Shirt', 20) has been added
Item('Shirt', 20) has been added
Item('Shoe', 50) has been added
Item('Jeans', 25) has been added

We've mentioned that the `__repr__` method can recreate the original object. Let's see how this is done:

# Call the __repr__ on an Item
shoe.__repr__()
"Item('Shoe', 50)"
# Check the type returned by the __repr__ method
shoe.__repr__().__class__.__qualname__
'str'
# Use the eval function to recreate the original object
eval(shoe.__repr__())
Item('Shoe', 50)
# Check the type returned with the eval function
eval(shoe.__repr__()).__class__.__qualname__
'Item'
# Get the name and price attribute
eval(shoe.__repr__()).name, eval(shoe.__repr__()).price
('Shoe', 50)
# delete the cart1
del cart1
Deleting ShoppingCart Number1

The `__repr__` method can recreate the original object with the `eval` function. You can't do it with the `__str__` built-in function.

Now, let's use the `__str__` method to display a user-friendly message of the items and the `__len__` method to get the number of items in the cart:

class ShoppingCart(object):
    
    __no_carts = 0
    
    def __init__(self):
        print("Initializing the ShoppingCart")
        self.items = []
        ShoppingCart.__no_carts += 1
        
    def __del__(self):
        print(f"Deleting ShoppingCart Number {self.get_cart_number()}")
        if ShoppingCart.__no_carts != 0:
            ShoppingCart.__no_carts -= 1
        else:
            print("Not Implemented")
            
    def __str__(self):
        if self.items:
            items = [(item.name, item.price) for item in self.items]
            print("The item(s) in the ShoppingCarts:", end="\n\n")
            for one_item in items:
                print("A {} that cost ${}".format(*one_item))
            return ""
        return "The ShoppingCart is Empty"
    
    def __len__(self):
        return len(self.items)
               
    @classmethod
    def get_cart_number(cls):
        return cls.__no_carts
    
    def add_item(self, item):
        self.items.append(item)
        print(f"{item} has been added")
        
    def remove_item(self, item):
        if item in self.items:
            self.items.remove(item)
            print(f"{item} has been removed")
        else:
            print(f"{item} not found in cart")

Next, let's create the cart and add the items to it:

cart1 = ShoppingCart()

cart1.add_item(shirt1)
cart1.add_item(shirt2)
cart1.add_item(shoe)
cart1.add_item(jeans)
Initializing the ShoppingCart
Item('Shirt', 20) has been added
Item('Shirt', 20) has been added
Item('Shoe', 50) has been added
Item('Jeans', 25) has been added
# Let's print the cart to display the items in it:
print(cart1)
The item(s) in the ShoppingCarts:
A Shirt that cost $20
A Shirt that cost $20
A Shoe that cost $50
A Jeans that cost $25
# Let's remove items from the cart
cart1.remove_item(shirt2)
Item('Shirt', 20) has been removed
# Get the number of items in the cart
print(f"The number of items in the cart is: {len(cart1)}")
The number of items in the cart is: 3
# Delete the cart
del cart1
Deleting ShoppingCart Number1

More magic methods

We can use other class method objects in the `ShoppingCart`. Suppose you've made an entry with the wrong price, you can make the price correction by implementing the `__setitem__` magic method. What if you have two carts and want to combine the items in one of the carts before checkout? Use the `__iadd__` magic method:

class ShoppingCart(object):
    
    __no_carts = 0
    
    def __init__(self):
        print("Initializing the ShoppingCart")
        self.items = []
        ShoppingCart.__no_carts += 1
        
    def __del__(self):
        print(f"Deleting ShoppingCart Number {self.get_cart_number()}")
        if ShoppingCart.__no_carts != 0:
            ShoppingCart.__no_carts -= 1
        else:
            print("Not Implemented")
            
    def __str__(self):
        if self.items:
            items = [(item.name, item.price) for item in self.items]
            print("The item(s) in the ShoppingCarts:", end="\n\n")
            for one_item in items:
                print("A {} that cost ${}".format(*one_item))
            return ""
        return "The ShoppingCart is Empty"
    
    def __len__(self):
        return len(self.items)
    
    def __setitem__(self, item_name, value):
        
        found_item = None
        for item in self.items:
            if item.name == item_name:
                found_item = item
                break
        
        if found_item:
            if value >= 0:
                found_item.price = value
            else:
                print("ValueError: Price cannot be negative")
        else:
            print(f"Item '{item_name}' not found in cart")
            
    def __iadd__(self, other):
        if other.__class__.__qualname__ == 'ShoppingCart':
            self.items.extend(other.items)
            other.items.clear()
            return self
        return f"{other} should be an instance of ShoppingCart"

        
    @classmethod
    def get_cart_number(cls):
        return cls.__no_carts
    
    def add_item(self, item):
        self.items.append(item)
        print(f"{item} has been added")
        
    def remove_item(self, item):
        if item in self.items:
            self.items.remove(item)
            print(f"{item} has been removed")
        else:
            print(f"{item} not found in cart")

    def checkout(self):
        total_price = sum(item.price for item in self.items)
        return f"Checkout completed. Total price: ${total_price}"

Let's see how this work by creating two carts. The first cart contains two shirts, a shoe, and a pair of jeans. The second cart includes a pair of headphones:

cart1 = ShoppingCart()

cart1.add_item(shirt1)
cart1.add_item(shirt2)
cart1.add_item(shoe)
cart1.add_item(jeans)


headphone = Item("Headphone", 20)

cart2 = ShoppingCart()
cart2.add_item(headphone)
Initializing the ShoppingCart
Item('Shirt', 20) has been added
Item('Shirt', 20) has been added
Item('Shoe', 50) has been added
Item('Jeans', 25) has been added
Initializing the ShoppingCart
Item('Headphone', 20) has been added

We added a headphone costing 20 in cart2. We want 40 pairs of headphones instead. We correct as follows:

cart2["Headphone"] = 40
print(cart2)
The item(s) in the ShoppingCarts:
A Headphone that cost $40

Next, we added items in cart2 to cart1:

cart1 += cart2
print(cart1)
The item(s) in the ShoppingCarts:
A Shirt that cost $20
A Shirt that cost $20
A Shoe that cost $50
A Jeans that cost $25
A Headphone that cost $40

Next, we checkout to get the total price of all the items in our cart:

Checkout completed. Total price: $155

After adding the contents in cart2 to cart1, cart2 is now empty. We can then delete this cart:

# Check if cart2 is empty
print(cart2.items)
[]
# Delete cart2
del cart2
Deleting ShoppingCart Number2
# Check that we are only left with one cart:
ShoppingCart.get_cart_number()
1

Conclusion

Python magic methods are a fantastic way to create flexible and expressive code that meets your needs. With these tools, you can customize your classes' behavior to perform exactly how you want. If you want to improve your Python programming skills, learning and utilizing these powerful concepts is essential.

In this article, we've explored the world of Python magic methods and learned how to construct a dynamic shopping cart. We've used a range of built-in Python functions to add and remove items, present cart contents in a user-friendly format, merge items from multiple carts and calculate the total price during checkout. The result is a highly adaptable, feature-rich shopping cart that can be customized to meet your unique requirements.

For further practice with Python magic methods and object-oriented programming, consider exploring the projects available in our Python track collections: Introduction to Python, Python Core, and more. You'll discover many engaging projects that align with your interests and aspirations. Embrace the enchanting power of Python code, unleash your boundless creativity, and allow this programming language to empower you in crafting truly awe-inspiring applications.

Related Hyperskill Topics

Share this article
Get more articles
like this
Thank you! Your submission has been received!
Oops! Something went wrong.

Create a free account to access the full topic

Wide range of learning tracks for beginners and experienced developers
Study at your own pace with your personal study plan
Focus on practice and real-world experience
Andrei Maftei
It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.