The property
decorator in Python allows you to define computed attributes in classes that act like regular attributes. This enables cleaner, more Pythonic code by avoiding explicit getter and setter methods.
In this comprehensive guide, we will cover the following topics:
Table of Contents
Open Table of Contents
What Are Computed Properties?
In object-oriented programming, a computed property (also known as derived property) is a property whose value is computed based on other data members rather than stored.
For example, let’s say we have a Person
class with first_name
and last_name
data attributes. We could compute a full_name
property by concatenating the first and last names.
Computed properties allow abstraction and encapsulation by hiding complexity behind simple, readable attributes. Developers using the class don’t need to know how the property is calculated under the hood.
The Need for Computed Properties
Here are some common cases where computed properties are useful in Python:
-
Data validation or preprocessing: Transform or validate data when setting a property’s value before storing it.
-
Derived data: Generate properties that depend on other property values. Avoid duplicating data already available.
-
Read-only properties: Provide get-only access to sensitive or internal data through a computed property.
-
Maintainability: Reduce complexity by computing a single property instead of repeating the same logic across your code.
-
Encapsulation: Hide internal state and expose a simple interface. Don’t let users directly access private attributes.
Without computed properties, we would need to implement these behaviors using getter and setter methods. But this can bloat the class with many single-purpose methods. The property
decorator offers a cleaner and more Pythonic approach.
Introducing the property
Decorator
The built-in property
decorator allows us to define computed properties in Python classes. Here is its signature:
property(fget=None, fset=None, fdel=None, doc=None)
The property
constructor takes up to 4 arguments, all optional:
-
fget
: The function to use for getting the value of this property. -
fset
: The function to use for setting the value of this property. -
fdel
: The function to use for deleting this property. -
doc
: The docstring for the property. Should describe behavior clearly.
The property
decorator handles turning these helper functions into computed attribute accesses on your class instances.
We will go over usage of these arguments in more detail in the following sections.
How to Use the property
Decorator
The property
decorator is placed directly above the method that you want to make a computed property. This method becomes the getter function.
Here is a simple example:
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
p = Person("John", "Doe")
print(p.full_name) # Calls the full_name() method
# Prints "John Doe"
We decorate the full_name()
method with @property
. This signals that full_name
should be accessed as an attribute instead of a method call.
Behind the scenes, Python runs the full_name
method and returns the value when we access p.full_name
. No need for an explicit get_full_name()
call!
Now let’s go over the 4 decorator arguments more in-depth, starting with the getter function.
The Getter Function
The getter function computes and returns the property’s value. This is required for a basic read-only property.
Here are some key notes on writing getter functions:
-
The getter function should take
self
as the first argument like a typical object method. -
Avoid side effects inside the getter function since it could be called frequently. Calculate the value and return it.
-
Raise
AttributeError
in the getter if the computed value is not valid or available. -
Use docstring to document the property if needed.
Let’s add a getter for a full_name_upper
property to our earlier example:
class Person:
# ...
@property
def full_name_upper(self):
"""Returns the full name in uppercase"""
if not self.full_name:
raise AttributeError("No full name assigned yet")
return self.full_name.upper()
Now we have a read-only computed property that raises an error if full_name
hasn’t been defined yet.
Property Setters and Deleters
To make a modifiable property, we can define a setter and/or a deleter function.
Setter Function
The setter allows assigning values to the property through attribute access.
@full_name.setter
def full_name(self, value):
self.first_name, self.last_name = value.split(' ', 1)
The setter takes self
and the new value
as arguments. It updates the underlying data members that the property relies on.
Use the setter for:
- Validating new values before assigning them
- Updating multiple members to maintain consistency
- Performing actions when the property is modified
Deleter Function
The deleter allows deleting the property using del
.
@full_name.deleter
def full_name(self):
self.first_name = None
self.last_name = None
This clears the first and last name attributes if del p.full_name
is called.
The deleter doesn’t take any arguments besides self
. Use it to reset dependent data members when the property is deleted.
Property Documentation
The doc
argument can be used to provide a docstring for your property.
@property
def full_name(self):
"""Read-only full name property"""
...
The docstring should clearly explain how the property functions and any side effects.
You can also set the docstring on the getter, setter or deleter methods instead.
Good documentation helps other developers use your computed properties correctly.
When to Use the property
Decorator
Now that we’ve covered how to use property
, let’s discuss when it’s most appropriate.
For “Computed” Properties
The property
decorator shines when creating properties whose values are computed in some way.
For example, these are all good candidates for property
:
- Full name from first + last name
- Age calculated from birth date
- Formatted phone number from raw phone number
- Total price from subtotals
Basically, use property
for any attribute that relies on a calculation or transformation of some kind.
For Encapsulation and Abstraction
The property
decorator allows encapsulation of internal state. Other code can read and modify properties without accessing private internal data directly.
It also enables abstraction by hiding implementation details behind a simple interface.
Think of properties as the public API for your class. Keep the internals private.
For Read-Only Attributes
The simplest way to create a read-only property is by adding @property
above a getter method without a setter.
Read-only properties give us more control over attributes and prevent accidental modification.
For example, identifiers, constants, and aggregated values often don’t need modification after initialization.
When You Need More Than Field Access
Sometimes assigning to an attribute should do more than just store the value.
For example, you may want to:
- Validate data before storage
- Parse values into appropriate types
- Compress or encrypt data
- Notify other objects of changes
The property
setter lets you handle these scenarios without giving up clean attribute access.
Advantages and Disadvantages
Let’s discuss some notable pros and cons of using the property
decorator:
Advantages:
-
Clean, idiomatic access syntax in Python. Noexplicit getter/setter calls.
-
Encapsulates logic behind attribute access. Simpler public interface.
-
Allows validation and handling logic when modifying properties.
-
Easier to refactor code as needs change. Don’t have to redesign public API.
-
Setters can update multiple dependent attributes at once atomically.
-
Getters can perform lazy evaluation rather than computing upfront.
Disadvantages:
-
More magic and indirection compared to explicit methods. Can make debugging tricky.
-
No parameters can be passed to property functions. Must rely on instance state.
-
Docstrings not visible in help/documentation by default for properties.
-
Subclasses may bypass property logic by directly accessing private names.
In most cases, the increased encapsulation and cleaner APIs outweigh the downsides. But consider both perspectives when deciding between properties and regular methods.
Real World Examples
To better illustrate practical use cases for the property
decorator, let’s go through some real-world examples:
1. Validating Data
We can validate data before setting it using the property
setter. For example:
class Person:
def __init__(self, age):
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("Age cannot be negative")
self._age = value
Now creating a Person
with a negative age will validate and raise an error rather than storing an invalid value.
2. Modifying Dependent Properties
Let’s model a car with integer fields for year, make, model and a formatted name property:
class Car:
def __init__(self, year, make, model):
self.year = year
self.make = make
self.model = model
@property
def name(self):
return f"{self.year} {self.make} {self.model}"
@name.setter
def name(self, value):
# Extract year, make, model from value
# Set respective fields
When a user sets car.name = "2012 Tesla Model S"
, the property setter decomposes this into the underlying fields.
3. Caching Expensive Lookups
We can use a property to cache data after performing an expensive computation or lookup:
class Image:
def __init__(self, path):
self.path = path
self._size = None
@property
def size(self):
if self._size is None:
# expensive size lookup
self._size = determine_image_size(self.path)
return self._size
The size
property getter performs the one-time lookup on first access and caches it. Subsequent accesses return the cached value instantly!
Common Mistakes
While the property
decorator is quite powerful, it’s easy to misuse it if you aren’t familiar with some of the pitfalls:
Forgetting to Call the Property
Don’t forget that a property must be called like a method even without arguments.
This is invalid:
p = Person()
print(p.full_name) # Broken! Forgot ()
You must call it like a method by adding ()
even if it takes no arguments:
print(p.full_name()) # Works!
Using a Property Like a Class Field
Properties do not behave exactly like regular class attributes. Avoid setting them in __init__
or class definitions:
# Broken! Sets attribute on class not instance
class Person:
full_name = "John Doe"
# Also broken!
p = Person()
p.full_name = "Jane Doe"
Treat properties like methods and call them to get/set values.
Forgetting to Return a Value
It’s a common pitfall for new Python programmers to forget that properties need to return a value:
@property
def full_name(self):
print(f"{self.first} {self.last}") # Oops!
This will run without error, but return None
instead of the name. Remember to use return
.
Causing Side Effects
Avoid side effects and mutable state in property getters and setters. Users may not expect repeated calls to the same property to produce different results.
Combining @property and @classmethod
The @property
decorator works on instance methods. For class-level properties, use @classmethod
instead:
@classmethod
@property
def max_users(cls):
return cls.max_active_users
Conclusion
The property
decorator is an elegant way to implement computed attributes in Python. It improves encapsulation and reduces boilerplate code compared to manual getter and setter methods.
Key takeaways:
- Use
property
for computed values based on instance state. - Setter allows validating data and modifying dependencies.
- Getter should avoid side effects and return the computed value.
- Provides a cleaner interface than explicit get/set methods.
- Improves encapsulation by hiding implementation details.
- Can be used for read-only attributes when no setter defined.
- Use docstrings to document your property’s behavior clearly.
While the property
decorator takes some experience to master, it’s a versatile tool that all Python programmers should know. Use it to write more Pythonic classes that provide a clean yet powerful interface.