Don’t use Python’s property
This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post for an introduction.
Attributing the problem
Suppose you’re writing Java and you write a class with an attribute :
public class MyClass {
public int value;
}
And then later on you realize that value
really wants to have some extra work happen at runtime — maybe it actually needs to be calculated “live” from other values, or needs auditing logic to note accesses, or similar. So what you actually want is something more like:
public class MyClass {
private int value;
public int getValue() {
// Do the work and return the value...
}
}
Except, at this point, you can’t turn the integer attribute someValue
into an integer-returning method someValue()
without forcing everything which used this class — which may be a lot of stuff, not all of which is yours — to adapt to this new interface and recompile.
So the standard recommendation in Java is never to declare a public attribute; always declare it private, and write methods to retrieve and set it (called “getters” and “setters”).
Python has a solution
In Python, on the other hand, you could write your class:
class MyClass:
value: int
def __init__(self, value: int):
self.value = value
And then when you later decide you need value
to be a method, you can do that without forcing anyone else to change their code that used your class, by declaring it as a “property”:
class MyClass:
_value: int
def __init__(self, value: int):
self.value = value
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, value: int):
self._value = value
A property
in Python lets you create a method — or up to three, for the operations to get, set and delete — which acts like a plain attribute. You don’t need to write parentheses to call it. For example, the following will work:
>>> m = MyClass(value=3)
>>> m.value
3
>>> m.value = 5
>>> m.value
5
But you (mostly) shouldn’t use it
If you have exactly the use case above — you started out with an attribute, and later it became more complex and now needs to be a method, but you don’t want to have to rewrite all code using that class — then you should use property
. That’s what it’s for (and in other languages, too; C#, for example, has built-in syntax for declaring wrappers similar to Python’s property
).
But way too much use of property
just comes down to “this is a method, and always was a method, I just wanted it to look like an attribute for aesthetic reasons”. Which is not a good use.
For example:
import math
class Circle:
center: tuple[float, float]
radius: float
def __init__(self, center: tuple[float, float], radius: float):
self.center = center
self.radius = radius
@property
def area(self):
return math.pi * (self.radius**2)
@property
def circumference(self):
return 2 * math.pi * self.radius
Using property
in this way can be deeply misleading, since it creates the impression of simple attribute access when in reality there might be complex logic — or even things like database queries or network requests! — going on. This is a necessary trade-off sometimes for plain attributes that later turn into methods (and should still be documented when it occurs), but when there’s no technical need to do this, you shouldn’t do it. In the example above, area()
and circumference()
should just be plain methods, not properties.
The one potential exception is for more complex use of descriptors — and Python’s property
is a relatively simple example of a descriptor — which let you create objects that emulate attribute behavior and offer fine-grained control of that behavior. Django’s ORM, for example, uses descriptors to let you read and assign fields of a model class in ways that look like plain attribute access although under the hood there may be data conversion or even delayed/deferred querying going on. And even that has its detractors: needing to remember to use select_related()
or prefetch_related()
to avoid numerous extra queries for related objects can be annoying, and is a common “gotcha” when working with Django’s ORM (SQLAlchemy, by contrast, sets the default relationship-loading behavior on the relationship).
In general, though, unless you really know what you’re doing with advanced descriptor use cases, or you have the very specific use case of turning a pre-existing attribute into a method, you probably should be avoiding property
, and writing descriptors in general, in your Python code.
Also, if you’re coming from a language like Java, don’t interpret this as “always write getter/setter methods from the start, just wrap them in property
” — always start with plain attributes, and only change to getter/setter methods if you need to later. The point of property
is that you can do so without breaking the API of your class.