Typorithm · medium

Validated properties and atomic assignment

pythonlibrariespackagingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Validated properties and atomic assignment

0%

Concept and contract

Use a property setter to protect an invariant without corrupting the previous valid value.

A property participates in Python's descriptor machinery, letting an attribute-looking interface delegate reads and writes to methods. That is useful when a small public API needs validation without exposing the storage field as the intended mutation path.

Score routes construction and subsequent assignment through the same setter. The contract accepts exact integers that are nonnegative, deliberately excluding booleans. Validation happens before updating _value, so a rejected assignment leaves the previous valid score intact. The leading underscore communicates internal storage convention; it is not a security boundary against arbitrary reflective access.

Mechanism and worked example

Construction assigns self.value, which invokes the setter instead of directly storing the input.

Construction assigns self.value, which invokes the setter instead of directly storing the input. The getter later reads _value. During an update, the function attempts the same public attribute assignment, catches its domain ValueError and reports the remaining value.

The cases distinguish a valid increase, a negative update and True. Python considers bool an int subclass, but this particular domain uses type(value) is int to exclude it. That restriction is a stated validation decision, not a universal recommendation that every numeric API reject boolean-compatible values.

Complete worked example

python

class Score:
    def __init__(self, value):
        self.value = value
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        if type(value) is not int or value < 0:
            raise ValueError('score must be a nonnegative integer')
        self._value = value

def inspect_example(initial, updated):
    score = Score(initial)
    try:
        score.value = updated
        error = None
    except ValueError:
        error = 'ValueError'
    return {'value': score.value, 'error': error}
Reasoning
Construction assigns self.value, which invokes the setter instead of directly storing the input. The getter later reads _value. During an update, the function attempts the same public attribute assignment, catches its domain ValueError and reports the remaining value.
Download the reusable Python and packaging computer lab ↗

Focused decision: use the complete example for context

python

@value.setter
def value(self, value):
    if type(value) is not int or value < 0:
        raise ValueError("invalid score")
    self._value = value
Decision
Extend the validated property to scores from zero through one hundred; rejected writes must preserve the previous value.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The controlled bug replaces the exact-type test with isinstance(value, int).

The controlled bug replaces the exact-type test with isinstance(value, int). True now passes the type gate and is stored. A truthy-looking value or a numerically equal one must not be accepted when the contract requires a genuine integer score.

Another tempting implementation stores the value before checking it. Raising afterward would report failure while leaving invalid state behind. The returned previous value in rejected-update cases helps protect the validate-before-write ordering. A property is only as reliable as the invariant and mutation path its implementation actually enforce.

Controlled bug: the executable contract rejects this version

python

class Score:
    def __init__(self, value):
        self.value = value
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError('score must be a nonnegative integer')
        self._value = value

def inspect_example(initial, updated):
    score = Score(initial)
    try:
        score.value = updated
        error = None
    except ValueError:
        error = 'ValueError'
    return {'value': score.value, 'error': error}
Reasoning
The controlled bug replaces the exact-type test with isinstance(value, int). True now passes the type gate and is stored. A truthy-looking value or a numerically equal one must not be accepted when the contract requires a genuine integer score.
Download the reusable Python and packaging computer lab ↗

Independent practice

Narrow the accepted score range to zero through one hundred, including both endpoints.

Narrow the accepted score range to zero through one hundred, including both endpoints. Preserve exact integer validation and reject every out-of-range update before assignment. The first hint is to combine lower and upper bounds within the existing condition.

The second hint is to compare the previous value after attempting one hundred and one. The worked baseline accepts that value, while the practice solution must retain the old score and report ValueError. Keep valid zero and ordinary positive updates working; do not implement the new limit by rejecting every update indiscriminately.

Step 1: Extend the validated property to scores from zero through one hundred; rejected writes must preserve the previous value. Step 2: Run the accepted baseline, edit the exercise, then check its independent solution cases; preserve caller inputs and the documented exception behavior.

Practice starter: solve before reading the solution

python

class Score:
    def __init__(self, value):
        self.value = value
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        if type(value) is not int or value < 0:
            raise ValueError('score must be a nonnegative integer')
        self._value = value

def inspect_example(initial, updated):
    score = Score(initial)
    try:
        score.value = updated
        error = None
    except ValueError:
        error = 'ValueError'
    return {'value': score.value, 'error': error}

# Task: Extend the validated property to scores from zero through one hundred; rejected writes must preserve the previous value.
# Practice contract: Extend the validated property to scores from zero through one hundred; rejected writes must preserve the previous value.
Reasoning
Extend the validated property to scores from zero through one hundred; rejected writes must preserve the previous value.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Narrow the accepted score range to zero through one hundred, including both endpoints. Preserve exact integer validation and reject every out-of-range update before assignment. The first hint is to combine lower and upper bounds within the existing condition.
  • Hint 2: The second hint is to compare the previous value after attempting one hundred and one. The worked baseline accepts that value, while the practice solution must retain the old score and report ValueError. Keep valid zero and ordinary positive updates working; do not implement the new limit by rejecting every update indiscriminately.

Solution, tests and trade-offs

The solution checks the complete range before storing _value.

The solution checks the complete range before storing _value. The acceptance cases therefore distinguish the old nonnegative contract from the new bounded contract, while preserving the boolean rejection and atomic state behavior. The same setter still serves construction and updates.

Properties make simple validation discoverable through a normal attribute interface. A more complex object may need an explicit method when a change affects several fields, performs I/O or can fail after partial work. Choose an interface that communicates those costs instead of hiding an entire transaction behind an apparently cheap attribute write.

Practice solution and acceptance checks

python

class Score:
    def __init__(self, value):
        self.value = value
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        if type(value) is not int or not 0 <= value <= 100:
            raise ValueError('score must be a nonnegative integer')
        self._value = value

def inspect_example(initial, updated):
    score = Score(initial)
    try:
        score.value = updated
        error = None
    except ValueError:
        error = 'ValueError'
    return {'value': score.value, 'error': error}
Reasoning
The solution checks the complete range before storing _value. The acceptance cases therefore distinguish the old nonnegative contract from the new bounded contract, while preserving the boolean rejection and atomic state behavior. The same setter still serves construction and updates.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain why the failed assignment must preserve the previous score.

Explain why the failed assignment must preserve the previous score. Then describe the roles of the getter, setter and internal storage attribute. Mention how the bool case differs between exact-type validation and an isinstance check in this domain.

For a stretch, add another related field and define whether their invariant requires one atomic operation. Demonstrate the risk of updating them through independent setters in a local fixture. Do not claim that a property makes concurrent mutation safe; synchronization and transaction boundaries need their own explicit design and verification.

Optional stretch — after the core task works: For a stretch, add another related field and define whether their invariant requires one atomic operation. Demonstrate the risk of updating them through independent setters in a local fixture. Do not claim that a property makes concurrent mutation safe; synchronization and transaction boundaries need their own explicit design and verification.

Computer practice: https://applydjinn.com/typorithm/python-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt, then run verify.py. Edit exercises/pye-descriptors.py and run --practice pye-descriptors. Fixture files are created in fresh temporary directories for every case. Packaging checks build real local artifacts, install the exact wheel without an index and run outside the source tree. No package is published to PyPI. The app includes static examples, prepared results and questions; it does not run Python or provide an editor.

Use a property setter to protect an invariant without corrupting the previous valid value.