Welcome to the Treehouse Community

The Treehouse Community is a meeting place for developers, designers, and programmers of all backgrounds and skill levels to get support. Collaborate here on code errors or bugs that you need feedback on, or asking for an extra set of eyes on your latest project. Join thousands of Treehouse students and alumni in the community today. (Note: Only Treehouse students can comment or ask questions, but non-students are welcome to browse our conversations.)

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and a supportive community. Start your free trial today.

Python

Property Setter Question

For this exercise, we're asked to build a setter for the price property by changing "_price". The exercise provides the following code:

class Product:
    _price = 0.0
    tax_rate = 0.12

    def __init__(self, base_price):
        self._price = base_price

    @property
    def price(self):
        return self._price + (self._price * self.tax_rate)

I'm wondering why this is an answer that the exercise 'Check Work' accepts:

    @price.setter
    def price(self, new_price):
        self._price = new_price

Instead of this...

    @price.setter
    def price(self, new_price):
        self._price = new_price/(1 + tax_rate)

Isn't the exercise checker wrong? I figured when a user types "[Product instance].price = 95", that the user is assigning 95 to 'price' ( calculated by self._price + (self._price * self.tax_rate) ) and not '_price' (which is just the base price)

[Sorry... last couple of lines should read:] Isn't the exercise checker wrong? I figured when a user types "[Product instance].price = 95" that the user is assigning 95 to "price" which is calculated using

self._price + self._price * self.tax_rate

and not assigning it to _price (which is just the base price)

6 Answers

Okay, so we are using setter to set the value of _price, which we cannot do like a normal member.

You can either:

    #As the question say: "Add a new setter (@price.setter) method to the Product class that updates the _price attribute."
    @price.setter
    def price(self, new_price):
        #replace price with new price.
        self._price = new_price

and update the price or you could do whatever manipulations you required to make.

But as mentioned in the question, here we just have to set the new price as passed in the arg.

Hi, Krishna- thanks so much for taking the time to answer. With this exercise, I think I understand the programming side of things... the "how to do" something. However, I'm wondering whether the answer should be different or the questioned changed.

If the user assigns a new value to self.price I would want self.price to be changed to that new value. I wouldnt want that new value to represent self._price (an entirely different variable) that is then multiplied by the tax_rate variable to then get 112% of the new value that the user inputted and assign it to self.price... that's doesn't seem very intuitive or efficient to me.

Fyi: My understanding is that .price in self.price is the method that behaves kinda like an attribute, thanks to the @property portion of code

Property cannot be set, to set the property we use setters. In above code there is no way you can set price(not the _price member, but the price property). Also, I believe the author wanted to introduce a concept and like he himself mentions most of the time, that these practices are more focused to give us a hang if it rather than a logical and optimal use of the concept.

That's a good point, Krishna, about teaching a concept vs. the most optimal solution. Still, there may be a better way to use a setter (and still be easy enough to teach the concept), so I'll loop in a moderator to see if they want to consider revising the exercise. Because, to use Kenneth's example in the video, setting self.price using self._price in the manner above is kinda like Kenneth typing "self.radius = 5" and 5 being assigned to self.diameter and calculating self.radius as 2.5 vs. the original input of 5. Below would be a way to fix:

   @price.setter
    def price(self, new_price):
        self._price = new_price/(1 + tax_rate)

Appreciate ALL your time in helping me so +1 to your answer : ) I leave the best answer unmarked for now.

Chris Freeman

Chris Freeman
Chris Freeman
Treehouse Moderator 68,166 Points

Go ahead and mark Krishna Pratap Chouhan's answer the best.

The main point of using properties is to establish a gatekeeper between the caller and the changing or reading attributes. The getter and setter methods can be as trivial as needed or they can perform as much admin overhead as needed. Another advantage of properties is that they can abstract the inner workings of the methods. This allows complex changes to be made inside the methods without affecting calling code. For example, a change in the tax_rate value or deciding to remove the tax calculation during the read of the property.

The @property establishes the method price as the "getter" for when the value Product.price is accessed like an attribute. As shown in initial challenge, the getter code already handles calculating the tax when the value is read. The advantage of calculating the tax on the read, versus during the set, is that it preserves the original price value in _price.

If the tax calculation is added to price.setter, then the read value would be doubly taxed.

Great question! Post back if you need more details!

Hi, Chris- good to hear from ya!

I think that whatever you do in the "getter", you sorta have to do the opposite of in the "setter" for these type of exercises. If you look at the Special Methods video (at 6:35), Kenneth essentially has self.radius = self.diameter/2 in the getter.... in the setter he has self.diameter = new_radius * 2. The getter has divided by 2 and the setter has multiplied by 2... which makes sense as the first one is assigning the radius and the other is assigning the diameter.

Likewise, in this exercise, the getter is essentially calculating self.price = self._price * (1 + self.tax_rate), so the setter must be self._price = self.price/(1 + self.tax_rate). One is multiplication and the other is division based on what we are assigning. This is because the getter includes tax, so the setter must exclude tax when assigning the _price variable. We could run a test of the code in the IDLE. For example, consider the following:

If you were to run this code....

p = Product(5)    #New instance
p.price = 6     #Reassigning price property
p.price     #asking the IDLE to evaluate the price property we just reassigned

in both of the Scenarios shown below, then you'll see that the As-IS solution [that is said to be correct] actually gives 6.72. Huh?? I thought we just set p.price = 6... why is Python telling us it is 6.72? Oops!!!! Because we didn't discount _price by the tax rate so that when the getter was called, it would factor in the tax rate and give us back 6.

BUT, if you evaluate p.price in Scenario 2, it will give 6. This is consistent with what Kenneth did in the video, and what the average user would have intended when typing "p.price = 6". After all, we want p.price to equal 6... not 6.72. Which is why I think the Setter exercise (either in the solution that the checker deems as right or by revising the instructions) may need to be revised. Hence the reason for my post... an attempt to be helpful... which has turned out to be an essay : ) Thanks, D.

Scenario 1: The As-Is price-setter Solution that the checker accepts

#[rest of code in exercise here]
@price.setter
    def price(self, new_price):
        self._price = new_price

Scenario 2: The proposed price.setter Solution that the checker rejects (yet I think this is actually the correct answer and Scenario 1 is wrong)

#[rest of code in exercise here]
    @price.setter
    def price(self, new_price):
        self._price = new_price/(1 + self.tax_rate)
Chris Freeman
Chris Freeman
Treehouse Moderator 68,166 Points

I see your point of wanting the solution to match the style of the radius-diameter example. In practice, each property depends on the specification of the task and may vary greatly.

The symmetry of the radius-diameter example is merely a coincidence of design. Because the getter and setter are radius-based and the underlying attribute is the diameter, the conversion from/to diameter is needed in both the getter and setter.

In the challenge, a non-symmetrical example shows that the setter can have a different construct than the getter. No hard reason other than showing it can be non-symmetrical. Is this challenge unusual? Perhaps. It depends on how much is read into the challenge wording verses just taking it at face value. In a real world situation, the coder could ask to confirm the intent of the specification. That's not possible in the challenge, of course.

How might your solution choice change if you could not see the details of the getter code and had to rely on just the wording of the challenge?

Another way the challenge could have been designed would be to use two properties—base_price and tax_price—where both properties have a symmetrical getter and setter. But that wouldn't show the non-symmetric possibilities.

Challenge wordings can get updated often to add clarification. Do you have a suggestion for how to improve the challenge wording to make it clearer that the setter doesn't need to adjust for taxes?

I'm curious if Kenneth Love has feedback on this?

Thanks, Chris- good point about teaching us students that a setter and getter can definitely be different from the pattern shown in the video. You know, I wish I had a suggestion for just a wording change (...that would be easier to revise, I'm sure), but I can't think of one at the moment that really does the trick.

As-is, the current wording "We need to be able to set the price of a product through a property setter. Add a new setter (@price.setter) method to the Product class that updates the _price attribute" tells me that the objective is to set price by updating _price using a property setter. (Besides, you wouldn't need a setter if you're ultimate goal was just to update _price, but you would if your ultimate goal was to update "price" by using _price.) So that's one of the reasons I likened it to setting the radius (i.e. price) by changing the diameter (i.e. _price) in Kenneth's video.

In a perfect world, I would recommend changing the solution and adding to the wording of the exercise "Don't forget that the getter adds in tax, so account for that in your setter!" Then Kenneth could make some pun like 'no taxation without representation' and we'd be golden! : )

Tatiane Lima
Tatiane Lima
6,738 Points

Here is what i did:

    @price.setter
    def price(self, price):
        self._price = price / round(self.tax_rate + 1)
Thomas Ross
Thomas Ross
4,634 Points

I got very stuck on this one, as well, and it was very frustrating. I'm glad I found this post, but was a bit disappointed with the solution. When this program is working correctly, here is what the user will experience:

>>> cog = Product(15)
>>> cog.price
16.8
>>> cog._price
15
>>> cog.price = 16.8
>>> cog.price
18.816000000000003

The price the user has just set is completely different than the price the user has just asked for. This is incredibly confusing.

The solution to this should be something like

@price.setter
def price(self, new_price):
    self._price = new_price / (1 + self.tax_rate)

where we are assuring that price is the total price that includes tax, and _price stores the pre-tax price.

I'd like to cast a vote for including the tax in the setter since the getter includes it.

@price.setter
def price(self, new_price):
    self._price = new_price + (new_price * self.tax_rate)
Thomas Ross
Thomas Ross
4,634 Points

The calculation you've used is a bit confusing:

In [3]: widget = Product(15)

In [4]: widget.price
Out[4]: 16.8

In [5]: widget._price
Out[5]: 15

In [6]: widget.price = 16.8

In [7]: widget.price
Out[7]: 21.07392

In [8]: widget._price
Out[8]: 18.816000000000003

Using self._price = new_price + (new_price * self.tax_rate), neither the price nor the _price match the value that I set as the price, and they are both way higher! I think we're on the same page the just setting _price = new_price via the @price.setter is confusing. Let me know if I'm missing something about your calculation.