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. ```python 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.gather for performance while keeping the resulting object's interface clean and synchronous. ```python @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) ```
Data Classes
Concepts
TL;DR
ArjanCodes (3 mentions) suggests Data Classes should focus on structuring information, as seen in videos like "Python Properties vs Methods: The Contract You Didn’t Know You Were Making."
- Feb 20, 2026
- Jul 25, 2025
- Oct 7, 2022
- Jun 3, 2022