Previous | Tutorial index | Next

πŸ“˜ TUTORIAL 10: THE property() FUNCTION FOR MANAGED ATTRIBUTES

Learning Objective

Understand the property() function and use it to add managed attributes.

10.1 Why Use Properties? The Problem with Direct Attribute Access

In OOP, encapsulation encourages hiding internal state and controlling access to it. Directly exposing attributes (obj.attribute) breaks encapsulation because:

The NaΓ―ve Approach

class Person: def __init__(self, name): self.name = name p = Person("Alice") print(p.name) # Fine p.name = "" # No validation – could be empty!

In languages like Java, you'd write getter and setter methods (getName(), setName()). But Python has a cleaner way: properties.

The Getter/Setter Pattern Without Properties (Not Pythonic)

class Person: def __init__(self, name): self._name = name def get_name(self): return self._name def set_name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value p = Person("Alice") print(p.get_name()) # Alice p.set_name("Bob") # OK # p.set_name("") # Raises ValueError

This works, but the API is clunky. Users must remember to use get_name() and set_name() instead of simple attribute access.

Properties solve this: they allow you to define methods that are accessed like attributes, preserving backward compatibility while adding control.

10.2 What is the property() Function?

The property() function is a built-in that creates a property attribute (a managed attribute) for a class. It returns a property object that can be assigned to a class attribute.

Syntax

property(fget=None, fset=None, fdel=None, doc=None)

If any argument is omitted, the corresponding operation is not allowed (e.g., setting or deleting raises an AttributeError).

How It Works

When you access obj.name, Python checks if name is a property; if so, it calls the stored getter. When you assign obj.name = value, it calls the setter, etc.

Basic Example: Read-Write Property

class Person: def __init__(self, name): self._name = name def get_name(self): return self._name def set_name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value name = property(get_name, set_name) p = Person("Alice") print(p.name) # Alice (calls get_name) p.name = "Bob" # Calls set_name # p.name = "" # Raises ValueError

Observation: The API is clean – p.name looks like a normal attribute, but it's actually invoking methods.

10.3 Read-Only Properties

To create a read-only property, provide only fget and omit fset and fdel.

class Circle: def __init__(self, radius): self._radius = radius def get_area(self): import math return math.pi * self._radius ** 2 area = property(get_area) c = Circle(5) print(c.area) # 78.5398... # c.area = 10 # Raises AttributeError: can't set attribute

This is useful for computed attributes that derive their value from other data.

10.4 Properties with Deleters

You can also define a deleter to control what happens when del obj.attribute is called.

class Person: def __init__(self, name): self._name = name def get_name(self): return self._name def set_name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value def del_name(self): print("Deleting name...") del self._name name = property(get_name, set_name, del_name) p = Person("Alice") del p.name # Prints "Deleting name..."

10.5 Adding Documentation with doc

The doc parameter can be used to provide a docstring for the property, which will show up in help().

def get_name(self): return self._name def set_name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value name = property(get_name, set_name, doc="The person's name. Cannot be empty.")

10.6 When to Use Properties

Properties are the Pythonic way to implement managed attributes. Use them when:

Example: Validated Age

class Person: def __init__(self, name, age): self._name = name self._age = age def get_name(self): return self._name def set_name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value def get_age(self): return self._age def set_age(self, value): if value < 0: raise ValueError("Age cannot be negative") self._age = value name = property(get_name, set_name) age = property(get_age, set_age) p = Person("Alice", 25) p.age = 30 # OK # p.age = -5 # Raises ValueError

10.7 Computed Attributes

Properties are perfect for attributes that are calculated from other data.

Example: Rectangle Area

class Rectangle: def __init__(self, width, height): self._width = width self._height = height def get_area(self): return self._width * self._height def get_width(self): return self._width def set_width(self, value): if value <= 0: raise ValueError("Width must be positive") self._width = value def get_height(self): return self._height def set_height(self, value): if value <= 0: raise ValueError("Height must be positive") self._height = value width = property(get_width, set_width) height = property(get_height, set_height) area = property(get_area) # Read-only computed property r = Rectangle(4, 5) print(r.area) # 20 r.width = 6 print(r.area) # 30 (computed on the fly)

10.8 Benefits Over Direct Attribute Access

Feature Direct Attribute Property
Validation Not possible Yes, in setter
Read-only Not possible (unless using __slots__ or other tricks) Yes, omit setter
Computed values Need a separate method Yes, property looks like an attribute
Backward compatibility If you change internal storage, external code breaks You can change _attr while keeping the property interface
Debugging/logging Hard to intercept access Easy: add print statements in getter/setter

10.9 Common Pitfalls and Best Practices

Pitfall Explanation How to Avoid
Forgetting to store the property in the class You must assign the result of property() to a class attribute. Use name = property(get_name, set_name) at class level.
Using self._name vs self.name in getter/setter If you use self.name inside the setter, you'll cause infinite recursion. Always access the underlying storage (_name) in the getter/setter, not the property itself.
Not checking types in setter If you expect a certain type, validate it. Add isinstance(value, str) checks.
Mixing property with normal attribute names If you have both _name and name property, ensure they don't conflict. Use a leading underscore for the stored attribute.
Using properties when a simple attribute is sufficient Over-engineering can make code harder to read. Use properties only when you need control or computation.
Omitting docstring Properties with no docstring are less self-documenting. Provide a docstring via the doc parameter or the decorator syntax (Tutorial 11).

10.10 Comparison with the Decorator Syntax

In Tutorial 11, you'll learn the @property decorator syntax, which is more concise and common. However, the property() function is still useful when:

Example of the decorator syntax for comparison:

class Person: def __init__(self, name): self._name = name @property def name(self): """The person's name.""" return self._name @name.setter def name(self, value): if not value: raise ValueError("Name cannot be empty") self._name = value

We will cover this in Tutorial 11.

10.11 Full Walkthrough Example: A Temperature Class

Let's build a Temperature class that stores temperature in Celsius but provides properties for Fahrenheit and Kelvin.

class Temperature: def __init__(self, celsius): self._celsius = celsius def get_celsius(self): return self._celsius def set_celsius(self, value): if value < -273.15: raise ValueError("Temperature below absolute zero") self._celsius = value def get_fahrenheit(self): return self._celsius * 9/5 + 32 def set_fahrenheit(self, value): celsius = (value - 32) * 5/9 self.set_celsius(celsius) # reuse validation def get_kelvin(self): return self._celsius + 273.15 def set_kelvin(self, value): celsius = value - 273.15 self.set_celsius(celsius) celsius = property(get_celsius, set_celsius, doc="Temperature in Celsius") fahrenheit = property(get_fahrenheit, set_fahrenheit, doc="Temperature in Fahrenheit") kelvin = property(get_kelvin, set_kelvin, doc="Temperature in Kelvin") # Usage t = Temperature(25) print(t.celsius) # 25 print(t.fahrenheit) # 77.0 print(t.kelvin) # 298.15 t.fahrenheit = 100 print(t.celsius) # 37.777... print(t.kelvin) # 310.927... # t.celsius = -300 # Raises ValueError

Observations:

This demonstrates how properties can provide a consistent interface while hiding the internal representation.

πŸ“ Quiz 10: The property() Function

1. What is the primary purpose of the property() function?

Answer(B) To create a managed attribute with getter, setter, and deleter methods.

2. Which of the following is NOT a valid argument to property()?

Answer(D) `finit`

3. What happens if you try to assign a value to a property that does not have a setter (fset omitted)?

Answer(B) An `AttributeError` is raised.

4. Consider the following code:

class MyClass: def __init__(self): self._x = 0 def get_x(self): return self._x def set_x(self, value): self._x = value x = property(get_x, set_x)

If you create an instance obj = MyClass(), what does obj.x refer to?

Answer(C) A property that calls `get_x`.

5. (True/False) A property can be used to create computed attributes that are recalculated each time they are accessed.

Answer(A) True

6. What is the correct way to add a docstring to a property created with property()?

Answer(B) `property(..., doc="text")`

7. What will happen if you define a setter that tries to assign to the property itself (e.g., self.name = value inside set_name)?

Answer(B) It will cause infinite recursion.

8. Which of the following is a valid use case for a property?

Answer(B) Validating input before storing.

9. If you want to make an attribute read‑only, which arguments do you pass to property()?

Answer(A) `fget` only

10. How does a property differ from a normal attribute?

Answer(B) A property can execute code when accessed or modified.

Below is the revised Exercise 10 and Homework 10 text with sample answers added. Each sample answer is provided inside a <details> block so that students can review them after attempting the tasks on their own.


πŸ§ͺ Exercise 10: Using property() for Validation and Computation

Part A: Create a BankAccount Class
Define a class BankAccount with:

Hint: In deposit, do self._balance += amount? But then you bypass the setter. To use the setter, do self.balance = self.balance + amount. However, this will call the getter and setter, which is okay but slightly slower. For consistency, you can store _balance directly but ensure the setter is used elsewhere. A better approach: define a private _set_balance method that the property setter calls.

Part B: Create a Rectangle with Properties
Define a Rectangle class with:

Part C: Test the Classes
Create instances and test all properties and methods, including edge cases (negative values, zero, etc.). Capture errors appropriately.

Sample Answers (Exercise 10)

Part A: BankAccount Class

class BankAccount: def __init__(self, initial_balance=0): # Use the setter to validate initial balance self.balance = initial_balance def _set_balance(self, value): """Internal method to set balance with validation.""" if value < 0: raise ValueError("Balance cannot be negative") self._balance = value @property def balance(self): return self._balance @balance.setter def balance(self, value): self._set_balance(value) @property def is_overdrawn(self): return self.balance < 0 def deposit(self, amount): if amount < 0: raise ValueError("Deposit amount cannot be negative") # Use the property setter to enforce validation self.balance = self.balance + amount def withdraw(self, amount): if amount < 0: raise ValueError("Withdrawal amount cannot be negative") # Use the property setter; it will raise if balance goes negative self.balance = self.balance - amount

Part B: Rectangle Class

class Rectangle: def __init__(self, width, height): self.width = width self.height = height @property def width(self): return self._width @width.setter def width(self, value): if value <= 0: raise ValueError("Width must be positive") self._width = value @property def height(self): return self._height @height.setter def height(self, value): if value <= 0: raise ValueError("Height must be positive") self._height = value @property def area(self): return self.width * self.height @property def perimeter(self): return 2 * (self.width + self.height)

Part C: Testing

# Test BankAccount acc = BankAccount(100) print(acc.balance) # 100 acc.deposit(50) print(acc.balance) # 150 acc.withdraw(200) # Raises ValueError: Balance cannot be negative # acc.balance = -10 # Also raises ValueError print(acc.is_overdrawn) # False # Test Rectangle rect = Rectangle(5, 4) print(rect.area) # 20 print(rect.perimeter) # 18 rect.width = 10 print(rect.area) # 40 # rect.width = -1 # Raises ValueError

🏠 Homework 10: Building a Product Inventory System with Properties

Task: Create a Product class that uses properties to manage its attributes with validation and computation.

Class Specifications

Private Attributes:

Properties:

  1. name – getter and setter.

  2. price – getter and setter.

  3. quantity – getter and setter.

  4. discount – getter and setter.

  5. total_value – read-only property.

  6. discounted_price – read-only property.

  7. total_discounted_value – read-only property.

Methods:

Sample Answers (Homework 10)

Complete Implementation of Product

class Product: def __init__(self, name, price, quantity=0, discount=0.0): # Use property setters to ensure validation self.name = name self.price = price self.quantity = quantity self.discount = discount @property def name(self): return self._name @name.setter def name(self, value): stripped = value.strip() if not stripped: raise ValueError("Name cannot be empty") self._name = stripped @property def price(self): return self._price @price.setter def price(self, value): # Try to convert to float try: val = float(value) except (TypeError, ValueError): raise ValueError("Price must be a number") if val < 0: raise ValueError("Price cannot be negative") self._price = val @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): try: val = int(value) except (TypeError, ValueError): raise ValueError("Quantity must be an integer") if val < 0: raise ValueError("Quantity cannot be negative") self._quantity = val @property def discount(self): return self._discount @discount.setter def discount(self, value): try: val = float(value) except (TypeError, ValueError): raise ValueError("Discount must be a number") if not (0 <= val <= 1): raise ValueError("Discount must be between 0 and 1") self._discount = val @property def total_value(self): return self.price * self.quantity @property def discounted_price(self): return self.price * (1 - self.discount) @property def total_discounted_value(self): return self.total_value * (1 - self.discount) def __str__(self): return (f"Product: {self.name}\n" f" Price: ${self.price:.2f}\n" f" Quantity: {self.quantity}\n" f" Discount: {self.discount*100:.1f}%\n" f" Total Value (before discount): ${self.total_value:.2f}\n" f" Discounted Price per unit: ${self.discounted_price:.2f}\n" f" Total Discounted Value: ${self.total_discounted_value:.2f}") # --- Testing --- if __name__ == "__main__": # Valid products p1 = Product("Laptop", 999.99, 5, 0.1) p2 = Product("Mouse", 25.50, 10) p3 = Product("Monitor", 199.99, 3, 0.05) print(p1) print(p2) print(p3) # Test validation try: p_bad = Product("", 10) except ValueError as e: print(f"Caught: {e}") try: p_bad = Product("Item", -5) except ValueError as e: print(f"Caught: {e}") try: p_bad = Product("Item", 10, -3) except ValueError as e: print(f"Caught: {e}") try: p_bad = Product("Item", 10, 5, 1.5) except ValueError as e: print(f"Caught: {e}") # Modify and see updates p1.quantity = 10 print(p1.total_value) # 9999.9 p1.price = 899.99 print(p1.total_discounted_value) # updated automatically

Homework Submission Requirements

Part 1: Code
Write the complete Product class with all properties and methods. Use the property() function (not the decorator syntax – save that for Tutorial 11). Include docstrings and comments.

Part 2: Testing Script
Create a test function that does the following:

  1. Create several products with valid data.
  2. Print each product using print(product) (which uses __str__).
  3. Attempt to set invalid values (empty name, negative price, negative quantity, discount out of range) and catch the exceptions.
  4. Display the total value and discounted price for each product.
  5. Change the discount for one product and show the updated values.
  6. Show that total value is recalculated automatically when price or quantity changes.

Part 3: Reflection Questions
Answer these in a comment block at the top of your script:

  1. Why is _price stored as a private attribute while price is a property? What would be the drawback of making price a public attribute?
  2. The total_value property is read-only. How would you implement it if you wanted to allow direct modification of total value that adjusts the quantity or price accordingly?
  3. What would happen if the setter for price used self.price = value instead of self._price = value? Why would that cause a problem?
  4. How does using properties make the class more maintainable? If you later decide to store price in cents instead of dollars, how would you change the class without breaking client code?
  5. In the __init__ method, we used the property setters (e.g., self.name = name) instead of directly assigning to _name. Why is this a good practice?

Bonus Challenge (Optional):
Add a class attribute all_products that is a list of all product instances. Add a class method total_inventory_value() that sums the total_discounted_value of all products. Ensure that the list is updated when a product is created and that the method works correctly.

πŸ“š Additional Resources for Self-Study

  1. Real Python: Python's property(): Add Managed Attributes to Your Classes – Comprehensive guide with examples.
  2. ZetCode: Python property() function – Concise reference.
  3. Python Official Docs: property() built-in – The definitive reference.
  4. GeeksforGeeks: Python property() function – Additional examples.

βœ… Summary Checklist for Tutorial 10

Before moving to Tutorial 11 (The @property Decorator), ensure you can confidently say YES to the following:

Ready for the next tutorial? In Tutorial 11, you will learn the decorator syntax for properties – the most common and Pythonic way to define properties using @property, @name.setter, and @name.deleter.

Previous | Tutorial index | Next