How does the `@property` decorator work in Python, and when should you prefer it over a plain attribute?
`@property` turns a method into a descriptor that Python calls automatically on attribute access, letting you add validation or computation behind a dot-access interface without changing callers. Use it when a value is derived, needs guarding, or must be lazily computed — not as a default for every attribute.
How to think about it
This question is testing whether you understand Python’s descriptor protocol and the design principle behind it. The real point: @property lets you start with a plain attribute and add behaviour later — validation, computation, caching — without changing a single line of code that calls your class.
Without @property, the only way to guard an attribute is to rename it to set_radius() and get_radius(), which forces every caller to update. With @property, c.radius = 5 still works but now runs your validation code invisibly.
A runnable walkthrough
How it works under the hood
@property creates a descriptor object attached to the class. When Python sees c.radius on the right-hand side of an assignment, it calls the descriptor’s __get__ method (your getter). When it sees c.radius = value, it calls __set__ (your setter). The private backing attribute _radius stores the actual value; the property mediates all access.
The class-level flow looks like this:
c.radius → Circle.radius.__get__(c, Circle) → return c._radius
c.radius = 10 → Circle.radius.__set__(c, 10) → validate + store c._radius = 10
When to use it
Use @property for derived values (like area or diameter), validated assignments, and lazy computation. Do not wrap every plain attribute in a property “just in case” — it adds overhead and noise with no benefit.