There's some new syntax in here, so let's look at things piece by piece:

NonNegative is a descriptor object. It's a descriptor because it defines the __get__ , __set__ , or __delete__ method.

The Movie class looks very clean. We create 4 descriptors at the class level, and treat them like normal (instance-level) attributes everywhere else. And apparently, the desciptors are checking for non-negative values for us.

Accessing a descriptor¶

When Python sees the line print m.budget , it recognizes that budget is a descriptor with a __get__ method. Instead of passing m.budget to print directly, it calls Movie.budget.__get__ , and feeds the result of that to print. This is similar to what happens when you access a property -- Python automatically calls a method, and returns the result.

__get__ receives two arguments: the instance object to the left of the period (that is, the m object in m.budget ), and the type of that instance ( Movie ). In some Python documentation, Movie is called the owner of the descriptor. If we had asked for Movie.budget , Python whould have called Movie.budget.__get__(None, Movie) ; that is, the fist argument is either an instance of the owner, or None . These input arguments may seem weird to you, but they're there to give you information about what object the descriptor is part of. This will make sense once we look inside the NonNegative class.

Assigning to a descriptor¶

When Python sees m.rating = 100 , Python recognizes rating is a descriptor with a __set__ method, and it calls Movie.rating.__set__(m, 100) . Like __get__ , the first argument of __set__ is the instance to the left of the period (the m in m.rating = 100 ). The second argument is the value to the right of the equals sign (100).

Deleting a descriptor¶

For the sake of completeness, if you call del m.budget , Python will call Movie.budget.__delete__(m) .

How NonNegative works¶

With this in mind, we can now look to see how the NonNegative class works. Each instance of NonNegative maintains a dictionary that maps owner instances to data values. When we call m.budget , the __get__ method looks up the data associated with m , and returns the result (or a default value, if no such value exists). __set__ uses the same approach, but includes the extra non-negativity check. We use a WeakKeyDictionary instead of a normal dict to prevent a memory leak -- we don't want an instance to stay alive simply because it's in the descriptor dictionary, and otherwise unused.

Working with descriptors is slightly awkward. Because they live at the class level, every instance shares the same descriptor. This means that descriptors have to manually manage different states for different object instances, and need to explicitly be passed instances as the first argument of the __get__ , __set__ , and __delete__ methods.

Hopefully, however, this example gives you an idea of what descriptors can be useful for -- they provide a way to organize property logic into isolated classes. If you find yourself repeating the same logic across several properties, that should be a clue to consider whether refactoring that code into a descriptor is worthwhile.