Attribute Access with Python’s Magic Methods

Python’s magic methods offer a powerful way to control how attributes are accessed and modified in your classes. By overriding these methods, you can customize the behavior of attribute retrieval and assignment, giving you greater flexibility in how your objects interact with the rest of your code.

In this post, we’ll explore three key magic methods that control attribute access in Python: __getattr__, __getattribute__, and __setattr__. We’ll use a Book class as an example to demonstrate how these methods work.

Controlling Attribute Retrieval with __getattribute__

The __getattribute__ method is called automatically when any attribute of an object is accessed. By overriding this method, you can perform operations or transformations on the attribute value before it is returned.

Here’s an example where we apply a discount to the price attribute whenever it is accessed:

				
					class Book:
    def __init__(self, title, author, price, discount=0.1):
        self.title = title
        self.author = author
        self.price = price
        self.discount = discount

    def __getattribute__(self, name):
        if name == 'price':
            price = super().__getattribute__('price')
            discount = super().__getattribute__('discount')
            return price - (price * discount)
        return super().__getattribute__(name)

    def __str__(self):
        return f"{self.title} by {self.author}, discounted price: {self.price}"

				
			

In this example, __getattribute__ checks if the attribute being accessed is price. If it is, it retrieves the actual price and discount values, applies the discount, and returns the adjusted price.

Validating Attribute Assignment with __setattr__

The __setattr__ method is called when an attribute’s value is set. This method allows you to validate or transform the value before it is assigned to the attribute.

Here’s how you can enforce that the price attribute must be a floating-point number:

				
					class Book:
    def __init__(self, title, author, price):
        self.title = title
        self.author = author
        self.price = price

    def __setattr__(self, name, value):
        if name == 'price':
            if not isinstance(value, float):
                raise ValueError("Price must be a float")
        super().__setattr__(name, value)

				
			

In this example, __setattr__ checks if the attribute being set is price and ensures that the value is a float. If the value isn’t a float, it raises a ValueError.

Handling Missing Attributes with __getattr__

The __getattr__ method is called when an attribute that doesn’t exist is accessed. This method provides a way to handle or generate attributes dynamically.

Here’s an example of using __getattr__ to provide a default message when accessing a non-existent attribute:

				
					class Book:
    def __getattr__(self, name):
        return f"{name} is not here"

    def __str__(self):
        return "This is a book"

				
			

In this example, if you try to access an attribute that doesn’t exist, __getattr__ returns a message indicating that the attribute is not present.

Testing the Implementation

Let’s see these methods in action:

				
					# Creating a book instance
b1 = Book("To Kill a Mockingbird", "Harper Lee", 38.95)

# Accessing the price attribute
print(b1.price)  # Should show the discounted price

# Setting the price attribute
try:
    b1.price = 30  # This will raise an exception because it's not a float
except ValueError as e:
    print(e)

b1.price = 30.0  # This will work
print(b1.price)  # Should show the discounted price

# Accessing a non-existent attribute
print(b1.random_attr)  # Should show "random_attr is not here"

				
			

Conclusion

Python’s magic methods for attribute access—__getattribute__, __setattr__, and __getattr__—provide powerful tools for customizing how attributes are accessed and modified. By leveraging these methods, you can enforce validation, apply transformations, and handle dynamic attributes with ease.

For more detailed information on these methods, refer to the official Python documentation: