Abstract flowing gradient in deep indigo and blue tones, smooth and luminous, evoking a modern digital learning atmosphere

Computer Science and programming articles. We do not sell courses.

Using Python's property decorator for getters and setters

Python’s built-in property decorator turns a class method into an attribute-style interface. A getter controls what is returned when code reads obj.name, while a setter controls what happens when code assigns obj.name = value. The public syntax stays familiar, but the class gains a place to validate, normalise, or record changes.

That controlled boundary is useful whenever an attribute has rules that should not be left to every caller. Instead of scattering checks across constructors, forms, and API handlers, you can place them beside the attribute definition. Python also supports computed and read-only properties, so the same mechanism can present derived data without duplicating it.

Why properties improve attribute control

A plain attribute stores exactly what it receives. That is often appropriate for passive data, but it can allow invalid states to spread through an application. A property uses the descriptor protocol behind @property: reading calls the getter, and assignment calls the setter if one is defined. The leading underscore on _name signals that it is the internal backing value rather than the intended public interface.

The distinction becomes especially useful in web applications, where model values pass between Python services, templates, and browser code. This web development guide covers that wider context. For an Australian fintech team in Sydney or Melbourne, for example, a balance may need to be stored as integer cents, validated on every update, and formatted differently depending on whether it is displayed in an API, an admin panel, or a customer statement.

Creating a getter and setter

Start with the attribute users should see and place the storage in a private-by-convention attribute. The decorator is applied to the getter first; the setter then reuses that property object through @celsius.setter.

class Temperature:
    def __init__(self, celsius: float) -> None:
        self.celsius = celsius

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if isinstance(value, bool) or not isinstance(value, (int, float)):
            raise TypeError("celsius must be a number")
        if not -80 <= value <= 60:
            raise ValueError("celsius must be between -80 and 60")
        self._celsius = float(value)

Reading sensor.celsius invokes the getter and returns a float, whereas sensor.celsius = 25 invokes the setter. Calling sensor.celsius() is incorrect because the property already supplies the value. Construction also benefits: self.celsius = celsius in __init__ passes through the same validation as every later assignment.

The setter returns None implicitly and communicates rejected input with exceptions. TypeError fits an unsupported type, while ValueError fits a number outside the accepted range. Keeping these failures explicit means callers receive a useful message at the point of assignment instead of encountering a malformed object later.

Validating values at the assignment boundary

Validation works best when it expresses a business invariant rather than an incidental preference. A setter can trim whitespace from a name, normalise an email address, require a non-empty string, or prevent a negative quantity. It should not silently repair ambiguous input, because a surprising correction can be more damaging than a clear rejection.

Privacy needs the same care. For organisations covered by Australia’s Privacy Act 1988, the Australian Privacy Principles influence how personal information is collected and managed; smaller businesses may also have state or territory privacy duties. A property can ensure that a date of birth is represented consistently or that a display name is always normalised, but it is not an access-control system. The _email backing attribute remains reachable inside the class, so sensitive records still require suitable permissions, storage controls, and audit logging.

The boundary is especially helpful for mutable values. Rather than returning a list that callers can modify without notice, a getter may return a copy or an immutable tuple. If you expose a mutable internal object directly, later validation can be bypassed. This approach also gives one reliable place to test valid values, missing fields, wrong types, and boundary cases, which supports teams that share code across a web service, a data pipeline, and a command-line tool.

Computed, read-only, and deletable properties

Not every property needs a setter. A getter can calculate a value from other state, ensuring that there is only one source of truth. For example, an order’s total can be derived from its line items rather than stored and updated separately. If the calculation is expensive, functools.cached_property is another option: it computes the result once, stores it on the instance, and reuses it for subsequent reads.

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    @property
    def area(self) -> float:
        return self.width * self.height

    @property
    def is_square(self) -> bool:
        return self.width == self.height

Because area and is_square have no setters, attempts to assign to them raise AttributeError. A deleter can be added with @area.deleter, but it should have a clear purpose, such as clearing a temporary session token. Deleting a property does not inherently erase copies, logs, or database records. Everyday systems such as Sydney’s Opal card and Melbourne’s Myki rely on defined handling of stored travel state, and application properties should likewise make deletion behaviour explicit rather than pretend that one line removes data everywhere.

Using properties with inheritance

Inherited properties behave like inherited attributes: a subclass receives the same getter and setter unless it overrides them. This makes a validated base class useful when several related objects share a rule. A Product base class can reject empty names, while subclasses add fields such as an author or network capacity without repeating that validation.

class Account:
    @property
    def balance(self) -> int:
        return self._balance

    @balance.setter
    def balance(self, value: int) -> None:
        if value < 0:
            raise ValueError("balance cannot be negative")
        self._balance = value


class SavingsAccount(Account):
    @Account.balance.setter
    def balance(self, value: int) -> None:
        if value < 100:
            raise ValueError("minimum opening balance is 100")
        super().balance = value

The subclass reuses the base property’s getter and replaces only its setter. In production code, this should follow a deliberate contract: a specialised subclass should not unexpectedly reject values the base class previously accepted unless that change is part of the public design. Calling the base implementation through super() avoids duplicating the base validation, while keeping each class’s extra rule visible.

Connecting properties to application layers

A property is a good boundary between domain rules and presentation. A layout component may need a width, an aspect ratio, or a ready-made CSS class; the model can supply those values without knowing whether a template, React component, or report renders them. For a concrete example of layout concerns outside the model, this pokies layout review shows why presentation deserves its own boundary.

Serialisation needs separate attention. Standard JSON encoding does not automatically include arbitrary properties, and dataclass helpers usually serialise declared fields rather than every computed attribute. An API layer should deliberately decide which fields leave the system. A method such as to_dict, a dedicated response model, or a library such as Pydantic can expose a stable external contract while the class continues to protect its internal representation.

The same discipline matters for Australian consumer-facing services. Australian Consumer Law makes accurate price and offer representations important for online retailers, so a product’s price_cents property can reject negative values and ensure that checkout code receives a valid number. This does not replace a legal review of discounts, taxes, terms, or guarantee claims; it simply prevents one common class of coding error from reaching customers. Local currency formatting should also be handled at the display boundary rather than stored as formatted text.

Reliable habits and testing practices

Properties are most valuable when the public interface remains simple and the rules stay close to the state they govern. They are less suitable for every field: a plain dataclass attribute may be clearer when no validation or derivation is needed, and a method may be better when an operation performs meaningful work. Choosing between an attribute, a property, and a method is part of API design, not merely syntax.

Static type checkers and unit tests should describe the intended contract. Type annotations on getter and setter parameters help, although runtime validation is still necessary for values arriving from JSON, forms, or other untyped boundaries. Tests should read the value, assign valid data, and confirm that invalid data raises the documented exception. A deleter needs its own test because it can affect persistence as well as memory.

Used this way, @property is more than shorthand for a getter. It creates a small, dependable contract between a class and the code that uses it, keeping validation close to the data while preserving the readable attribute syntax familiar throughout Python.