Python Properties
Controlling Attribute Access: Understanding Properties
In Python, you often want to control how the data (attributes) of your objects are accessed or changed. Sometimes you want to add logic when getting or setting a value, like checking for valid data or updating something else automatically. This is where properties come in.
This article will show you what properties are, why they are useful, and how to use them in your own classes.
What Is a Property?
A property lets you define special methods in a class that are used when you get, set, or delete an attribute. Properties make it possible to add logic to attribute access, while still letting you use simple dot notation (obj.value
).
Properties are most often used to:
- Validate data before setting it
- Make an attribute read-only
- Compute a value on the fly
- Run code when an attribute is changed
Getters and Setters Without Properties
Before properties, the common way to control access to an attribute in Python was to write explicit methods for getting and setting the value. These are usually named get_attribute
and set_attribute
.
Example:
class Person:
def __init__(self, name: str) -> None:
self._name: str = name
def get_name(self) -> str:
return self._name
def set_name(self, value: str) -> None:
if not value:
raise ValueError("Name cannot be empty!")
self._name = value
p = Person("Alice")
print(p.get_name()) # Output: Alice
p.set_name("Bob") # Works fine
# p.set_name("") # Raises ValueError
- You have to call
get_name()
andset_name()
instead of usingp.name
andp.name = ...
.
Problems with this approach:
- The code is less readable and less Pythonic. You can't use simple attribute access.
- If you change your class to use get/set methods, all code using your class must also change.
- You lose the simplicity and clarity of dot notation (
p.name
). - It's easy to forget to use the methods and accidentally access the attribute directly.
Properties solve these problems by letting you add logic to attribute access while keeping the same simple syntax.
What Are Getters and Setters?
A getter is a method that is used to read (get) the value of an attribute. It lets you control what happens when someone accesses the attribute, such as returning a computed value or adding a print statement for debugging.
A setter is a method that is used to set (change) the value of an attribute. It lets you control what happens when someone assigns a new value, such as checking if the value is valid or updating related data.
- The getter is called automatically when you write
value = obj.attribute
. - The setter is called automatically when you write
obj.attribute = new_value
.
With properties, you can define both a getter and a setter for the same attribute name, so you can add logic to both reading and writing the value, while still using simple attribute access.
Defining a Property with @property
The easiest way to create a property is with the @property
decorator. You define a method with @property
to make it act like a getter (for reading the value). You can also define setter and deleter methods for writing or deleting the value.
Example: Making an Attribute Read-Only
class Circle:
def __init__(self, radius: float) -> None:
self._radius: float = radius
@property
def radius(self) -> float:
return self._radius
@property
def diameter(self) -> float:
return self._radius * 2
c = Circle(5.0)
print(c.radius) # Output: 5.0
print(c.diameter) # Output: 10.0
# c.radius = 10.0 # ERROR: can't set attribute (no setter defined)
radius
anddiameter
are properties. You use them like attributes, but they are actually methods.radius
is read-only because there is no setter.
Adding a Setter to a Property
If you want to allow changing the value, add a setter method using @property_name.setter
.
Example: Validating Data When Setting
class Person:
def __init__(self, name: str) -> None:
self._name: str = name
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if not value:
raise ValueError("Name cannot be empty!")
self._name = value
p = Person("Alice")
print(p.name) # Output: Alice
p.name = "Bob" # Works fine
# p.name = "" # Raises ValueError
- The setter lets you add checks before changing the value.
Why Use Properties?
- Encapsulation: Hide the internal details of how data is stored.
- Validation: Check or clean data before setting it.
- Read-only or computed attributes: Make some values read-only, or calculate them on the fly.
- Backward compatibility: Change how an attribute works without changing how you use it in code.
Defining Properties the Old Way (property())
You can also create properties using the built-in property()
function. This is less common now, but you may see it in older code.
from typing import Any
def get_x(self) -> Any:
return self._x
def set_x(self, value: Any) -> None:
self._x = value
class Example:
x = property(get_x, set_x)
Common Beginner Mistakes
- Forgetting to use an underscore (like
_value
) for the real data, causing infinite recursion. - Not defining a setter when you want to allow changing the value.
- Trying to set a read-only property (will raise an error).
Example of a mistake:
class Bad:
@property
def value(self) -> Any: # Added type hint
return self.value # ERROR: calls itself forever!
Summary
- Use
@property
to control how attributes are accessed or changed. - Properties let you add logic to getting, setting, or deleting attributes.
- Use a setter (
@name.setter
) to allow changing the value. - Properties help with validation, encapsulation, and computed values.
Assignment: Practice Time!
1. Create a class Temperature
with:
- An attribute for Celsius (store as
_celsius
). - A property
celsius
for getting and setting the value (raise an error if below -273.15). - A read-only property
fahrenheit
that returns the temperature in Fahrenheit.
2. Try creating a temperature, changing it, and printing both Celsius and Fahrenheit.
class Temperature:
def __init__(self, celsius: float) -> None:
self.celsius = celsius # Use the setter for initial validation
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero (-273.15 C)!")
self._celsius: float = value
@property
def fahrenheit(self) -> float:
return (self.celsius * 9/5) + 32
# Example Usage:
temp = Temperature(25.0)
print(f"{temp.celsius}°C is {temp.fahrenheit}°F")
temp.celsius = 100.0
print(f"{temp.celsius}°C is {temp.fahrenheit}°F")
# This would raise a ValueError:
# temp.celsius = -300.0