Previous | Tutorial index | Next
property() FUNCTION FOR MANAGED ATTRIBUTESUnderstand the property() function and use it to add managed attributes.
In OOP, encapsulation encourages hiding internal state and controlling access to it. Directly exposing attributes (obj.attribute) breaks encapsulation because:
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.
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.
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.
property(fget=None, fset=None, fdel=None, doc=None)
fget β function to get the value (getter).fset β function to set the value (setter).fdel β function to delete the value (deleter).doc β documentation string for the property.If any argument is omitted, the corresponding operation is not allowed (e.g., setting or deleting raises an AttributeError).
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.
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.
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.
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..."
docThe 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.")
Properties are the Pythonic way to implement managed attributes. Use them when:
_name as a first/last name later).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
Properties are perfect for attributes that are calculated from other data.
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)
| 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 |
| 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). |
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.
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:
celsius property stores the actual value.fahrenheit and kelvin are computed properties with setters that convert back to Celsius.set_celsius).This demonstrates how properties can provide a consistent interface while hiding the internal representation.
property() Function1. What is the primary purpose of the property() function?
2. Which of the following is NOT a valid argument to property()?
fgetfsetfdelfinit3. What happens if you try to assign a value to a property that does not have a setter (fset omitted)?
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?
_x attribute.get_x.get_x function.5. (True/False) A property can be used to create computed attributes that are recalculated each time they are accessed.
6. What is the correct way to add a docstring to a property created with property()?
property(doc="text")property(..., doc="text")property.__doc__ after creation.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)?
RecursionError.TypeError.8. Which of the following is a valid use case for a property?
9. If you want to make an attribute readβonly, which arguments do you pass to property()?
fget onlyfget and fsetfset onlyfdel only10. How does a property differ from a normal attribute?
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.
property() for Validation and ComputationPart A: Create a BankAccount Class
Define a class BankAccount with:
_balance.balance with:
_balance.ValueError.is_overdrawn (read-only) that returns True if balance < 0.deposit(amount) and withdraw(amount) that modify _balance (but you can also use the setter via self.balance += amount? Be careful with recursion). For simplicity, implement the methods to update _balance directly, but ensure the property's setter is used for validation.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:
_width and _height.width and height with getters and setters that validate positive values.area.perimeter.Part C: Test the Classes
Create instances and test all properties and methods, including edge cases (negative values, zero, etc.). Capture errors appropriately.
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
Task: Create a Product class that uses properties to manage its attributes with validation and computation.
Private Attributes:
_name (str)_price (float) β must be non-negative._quantity (int) β must be non-negative._discount (float) β discount percentage (0 to 1, default 0.0).Properties:
name β getter and setter.
ValueError.price β getter and setter.
ValueError.quantity β getter and setter.
ValueError.discount β getter and setter.
ValueError.total_value β read-only property.
price * quantity (before discount).discounted_price β read-only property.
price * (1 - discount).total_discounted_value β read-only property.
total_value * (1 - discount).Methods:
__init__(self, name, price, quantity=0, discount=0.0) β initialize using property setters (so validation runs).__str__(self) β returns a nicely formatted string with all details.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
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:
print(product) (which uses __str__).Part 3: Reflection Questions
Answer these in a comment block at the top of your script:
_price stored as a private attribute while price is a property? What would be the drawback of making price a public attribute?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?price used self.price = value instead of self._price = value? Why would that cause a problem?__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.
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.