Python Properties vs Methods: The Contract You Didn’t Know You Were Making
The Psychological Contract of Object Access
Choosing between a property and a method in Python isn't just a matter of syntax; it's about the promise you make to the caller. When you use a property via the @property decorator, you communicate that the access is cheap, safe, and deterministic. It feels like an attribute, so the user assumes they can read it repeatedly without a performance penalty or a crash.

Methods tell a different story. They signal that work is happening. A method call implies the potential for complexity, side effects, or external communication with a database. If your code performs heavy computation or network I/O, hiding it behind a property breaks the mental model of the developer using your API.
Implementation and the Descriptor Protocol
Under the hood, Python uses the descriptor protocol to make properties work. This protocol defines how objects behave when accessed or modified. A property is effectively a method disguised as an attribute. While this allows for clean syntax, it requires discipline. For example, a property is inherently read-only unless you explicitly define a setter using the @property_name.setter syntax.
class UserAccount:
def __init__(self, status: str):
self._status = status
@property
def is_active(self) -> bool:
return self._status == "active"
@is_active.setter
def is_active(self, value: bool):
self._status = "active" if value else "closed"
The Danger of Hidden I/O
You might feel tempted to trigger database saves inside a property setter. Resist this. Performing persistence or network calls inside a property violates the principle of least astonishment. If a database call fails or blocks, the caller won't expect an attribute assignment to be the culprit. Always keep I/O explicit. Use a property to update local state and a dedicated save() method to handle the actual persistence. This allows for batching updates and keeps your object's behavior predictable.
Async Properties: Just Because You Can Doesn't Mean You Should
Technically, Python allows you to define an async property. You can use await on an attribute access, but this is a massive design smell. Async operations imply scheduling and potential failure—the exact opposite of what a property represents.
Instead of async properties, use an asynchronous class method to load data into a simple data class. This pattern separates the "work" of fetching data from the "state" of the object itself. You get the benefits of asyncio for performance while keeping the resulting object's interface clean and synchronous.
@dataclass
class UserAccount:
username: str
status: str
@classmethod
async def load(cls, user_id: int):
# Perform async I/O here
username, status = await asyncio.gather(
repo.fetch_name(user_id),
repo.fetch_status(user_id)
)
return cls(username, status)
- Python
- 43%· languages
- asyncio
- 14%· libraries
- Data Classes
- 14%· concepts
- Descriptor Protocol
- 14%· concepts
- Java
- 14%· languages

Python Properties vs Methods: The Contract You Didn’t Know You Were Making
WatchArjanCodes // 17:05
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!