{"id":"encapsulation-setters","slug":"setters","title":"Setters that check the value","text":"To let the client change a property with an ordinary assignment, add a **setter**: a second method with the same name, with the decorator `@name.setter` above it. The setter gets the new value as a parameter, so it can check the value before storing it:\n\n```python\nclass Thermostat:\n    def __init__(self):\n        self.__target = 20\n\n    @property\n    def target(self):\n        return self.__target\n\n    @target.setter\n    def target(self, degrees):\n        if 5 <= degrees <= 30:\n            self.__target = degrees\n\n\nheater = Thermostat()\nheater.target = 23\nprint(heater.target)\nheater.target = 45\nprint(heater.target)\n```\n\n```output\n23\n23\n```\n\nFor the client, `heater.target = 23` looks just like assigning to an attribute. Behind the scenes the setter runs, and it quietly ignores 45.\n\nThe getter has to come first. `@property` creates the property called `target`, and `@target.setter` then adds a setter to it. With the setter first, Python stops at the decorator with `NameError: name 'target' is not defined`, because no `target` exists yet.\n\n### Saying no out loud\n\nIgnoring a bad value hides the mistake: the client thinks the heater is set to 45. It's usually better to **raise** an exception, as you did in part 6. A `ValueError` is the usual one for a value that isn't acceptable:\n\n```python\nclass Thermostat:\n    def __init__(self):\n        self.__target = 20\n\n    @property\n    def target(self):\n        return self.__target\n\n    @target.setter\n    def target(self, degrees):\n        if degrees < 5 or degrees > 30:\n            raise ValueError(\"The target must be from 5 to 30 degrees\")\n        self.__target = degrees\n\n\nheater = Thermostat()\nheater.target = 23\nprint(heater.target)\nheater.target = 45\n```\n\n```output\n23\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 19, in <module>\n    heater.target = 45\n    ^^^^^^^^^^^^^\n  File \"main.py\", line 12, in target\n    raise ValueError(\"The target must be from 5 to 30 degrees\")\nValueError: The target must be from 5 to 30 degrees\n```\n\nThe client can catch the error with `try` and `except ValueError` and decide what to do, for example ask the user again.\n\n### The setter in `__init__`\n\nThe value given to the constructor needs the same check. Instead of writing the check twice, let `__init__` use the setter: `self.target = degrees`, without the underscores, assigns through the property and so runs the setter.\n\n```python\nclass Thermostat:\n    def __init__(self, degrees):\n        self.target = degrees  # runs the setter, which checks the value\n```\n\nNow `Thermostat(45)` raises the same `ValueError`, and no thermostat can ever start with a bad target.\n\nOne trap: inside the setter, store the value in the private attribute, `self.__target = degrees`. Writing `self.target = degrees` there would call the setter again, which would call itself again, and so on, until Python stops with a `RecursionError`.\n","task":"A product's price must never be negative. Fill in the blanks so that `price` has a setter that raises a `ValueError`, and the program prints:\n\n```\n35\nError: The price can't be negative\n35\n```\n","kind":"fill","seed":"class Product:\n    def __init__(self, name, price):\n        self.name = name\n        self.__price = price\n\n    @property\n    def price(self):\n        return self.__price\n\n    @[[price]].setter\n    def price(self, price):\n        if price < 0:\n            raise [[ValueError]](\"The price can't be negative\")\n        self.__price = price\n\n\nlamp = Product(\"Lamp\", 40)\nlamp.price = 35\nprint(lamp.price)\ntry:\n    lamp.price = -5\nexcept ValueError as error:\n    print(\"Error:\", error)\nprint(lamp.price)\n","solution":"class Product:\n    def __init__(self, name, price):\n        self.name = name\n        self.__price = price\n\n    @property\n    def price(self):\n        return self.__price\n\n    @price.setter\n    def price(self, price):\n        if price < 0:\n            raise ValueError(\"The price can't be negative\")\n        self.__price = price\n\n\nlamp = Product(\"Lamp\", 40)\nlamp.price = 35\nprint(lamp.price)\ntry:\n    lamp.price = -5\nexcept ValueError as error:\n    print(\"Error:\", error)\nprint(lamp.price)\n","hints":["A setter's decorator is the property's name, a dot and `setter`.","The decorator is `@price.setter`, and the error to raise is a `ValueError`, which the `except` below catches."],"tests":[{"name":"Output","stdin":"","expect":"35\nError: The price can't be negative\n35","hidden":false}],"require":[],"forbid":[],"more":[{"task":"Write the class `Book`. Its constructor takes a title and a rating:\n\n- The title is read-only: a property `title` with a getter only.\n- The rating is a property `rating` with a getter and a setter. A rating must be a number from 1 to 5; for any other value the setter raises `ValueError(\"The rating must be from 1 to 5\")`.\n- The constructor must check the rating too, so it should use the setter.\n\nFor example, this code:\n\n```python\nbook = Book(\"Small Gods\", 4)\nbook.rating = 5\nprint(book.title, book.rating)\ntry:\n    book.rating = 9\nexcept ValueError as error:\n    print(error)\nprint(book.rating)\n```\n\nprints:\n\n```\nSmall Gods 5\nThe rating must be from 1 to 5\n5\n```\n","kind":"code","seed":"class Book:\n    def __init__(self, title, rating):\n        self.__title = title\n        self.__rating = rating\n\n\nif __name__ == \"__main__\":\n    book = Book(\"Small Gods\", 4)\n    book.rating = 5\n    print(book.title, book.rating)\n    try:\n        book.rating = 9\n    except ValueError as error:\n        print(error)\n    print(book.rating)\n","solution":"class Book:\n    def __init__(self, title, rating):\n        self.__title = title\n        self.rating = rating\n\n    @property\n    def title(self):\n        return self.__title\n\n    @property\n    def rating(self):\n        return self.__rating\n\n    @rating.setter\n    def rating(self, rating):\n        if rating < 1 or rating > 5:\n            raise ValueError(\"The rating must be from 1 to 5\")\n        self.__rating = rating\n\n\nif __name__ == \"__main__\":\n    book = Book(\"Small Gods\", 4)\n    book.rating = 5\n    print(book.title, book.rating)\n    try:\n        book.rating = 9\n    except ValueError as error:\n        print(error)\n    print(book.rating)\n","hints":["Write a getter for each attribute (`@property`), and a setter for the rating only (`@rating.setter`, after its getter).","In the setter, raise the error when `rating < 1 or rating > 5`; otherwise store the value in `self.__rating`.","In `__init__`, write `self.rating = rating` without underscores, so the setter checks the starting rating too."],"tests":[{"name":"Test 1","stdin":"","call":"book = Book(\"Small Gods\", 4)\nbook.rating = 5\nprint(book.title, book.rating)\ntry:\n    book.rating = 9\nexcept ValueError as error:\n    print(error)\nprint(book.rating)\n","expect":"Small Gods 5\nThe rating must be from 1 to 5\n5","hidden":false},{"name":"Test 2","stdin":"","call":"try:\n    Book(\"Rain Atlas\", 0)\n    print(\"created\")\nexcept ValueError as error:\n    print(error)\nbook = Book(\"Field Notes\", 1)\nprint(book.rating)\ntry:\n    book.rating = 6\nexcept ValueError as error:\n    print(error)\nbook.rating = 3\nprint(book.rating)\n","expect":"The rating must be from 1 to 5\n1\nThe rating must be from 1 to 5\n3","hidden":true},{"name":"Test 3","stdin":"","call":"book = Book(\"Dawn\", 2)\ntry:\n    book.title = \"Dusk\"\nexcept AttributeError:\n    print(\"the title is read-only\")\nprint(book.title)\n","expect":"the title is read-only\nDawn","hidden":true}],"require":[],"forbid":[]},{"task":"Write the class `Temperature`. It stores degrees Celsius in a private attribute, and its constructor takes degrees Celsius. It has two properties, each with a getter and a setter:\n\n- `celsius`: nothing can be colder than -273.15 degrees, so for a lower value the setter raises `ValueError(\"Below absolute zero\")`. The constructor uses this setter too.\n- `fahrenheit`: the getter returns `celsius * 9 / 5 + 32`. The setter turns the Fahrenheit value it gets into Celsius, `(fahrenheit - 32) * 5 / 9`, and stores it through the `celsius` setter, so the same check applies.\n\nFor example, this code:\n\n```python\nwater = Temperature(25)\nprint(water.fahrenheit)\nwater.fahrenheit = 212\nprint(water.celsius)\ntry:\n    water.celsius = -300\nexcept ValueError as error:\n    print(error)\n```\n\nprints:\n\n```\n77.0\n100.0\nBelow absolute zero\n```\n","kind":"code","seed":"class Temperature:\n    def __init__(self, celsius):\n        pass\n\n\nif __name__ == \"__main__\":\n    water = Temperature(25)\n    print(water.fahrenheit)\n    water.fahrenheit = 212\n    print(water.celsius)\n    try:\n        water.celsius = -300\n    except ValueError as error:\n        print(error)\n","solution":"class Temperature:\n    def __init__(self, celsius):\n        self.celsius = celsius\n\n    @property\n    def celsius(self):\n        return self.__celsius\n\n    @celsius.setter\n    def celsius(self, degrees):\n        if degrees < -273.15:\n            raise ValueError(\"Below absolute zero\")\n        self.__celsius = degrees\n\n    @property\n    def fahrenheit(self):\n        return self.celsius * 9 / 5 + 32\n\n    @fahrenheit.setter\n    def fahrenheit(self, degrees):\n        self.celsius = (degrees - 32) * 5 / 9\n\n\nif __name__ == \"__main__\":\n    water = Temperature(25)\n    print(water.fahrenheit)\n    water.fahrenheit = 212\n    print(water.celsius)\n    try:\n        water.celsius = -300\n    except ValueError as error:\n        print(error)\n","hints":["Start with the `celsius` property: a getter that returns `self.__celsius`, and a setter that checks the value before storing it in `self.__celsius`.","`__init__` and the `fahrenheit` setter both store their value with `self.celsius = ...` (no underscores), so the `celsius` setter checks it.","The `fahrenheit` setter is one line: `self.celsius = (degrees - 32) * 5 / 9`."],"tests":[{"name":"Test 1","stdin":"","call":"water = Temperature(25)\nprint(water.fahrenheit)\nwater.fahrenheit = 212\nprint(water.celsius)\ntry:\n    water.celsius = -300\nexcept ValueError as error:\n    print(error)\n","expect":"77.0\n100.0\nBelow absolute zero","hidden":false},{"name":"Test 2","stdin":"","call":"cold = Temperature(-40)\nprint(cold.fahrenheit)\ncold.fahrenheit = 50\nprint(cold.celsius)\ntry:\n    cold.fahrenheit = -500\nexcept ValueError as error:\n    print(error)\nprint(cold.celsius)\n","expect":"-40.0\n10.0\nBelow absolute zero\n10.0","hidden":true},{"name":"Test 3","stdin":"","call":"try:\n    Temperature(-274)\nexcept ValueError as error:\n    print(error)\nprint(Temperature(-273.15).celsius)\nprint(Temperature(30).fahrenheit)\n","expect":"Below absolute zero\n-273.15\n86.0","hidden":true}],"require":[{"pattern":"@celsius\\.setter","raw":true,"message":"Give `celsius` a setter with `@celsius.setter`."},{"pattern":"@fahrenheit\\.setter","raw":true,"message":"Give `fahrenheit` a setter with `@fahrenheit.setter`."}],"forbid":[]}]}