Why does setting a descriptor on a class overwrite the descriptor?

You are correct that B.v = 3 simply overwrites the descriptor with an integer (as it should).

For B.v = 3 to invoke a descriptor, the descriptor should have been defined on the metaclass, i.e. on type(B).

>>> class BMeta(type): 
...     v = VocalDescriptor() 
>>> class B(metaclass=BMeta): 
...     pass 
>>> B.v = 3 

To invoke the descriptor on B, you would use an instance: B().v = 3 will do it.

The reason for B.v invoking the getter is to allow returning the descriptor instance itself. Usually you would do that, to allow access on the descriptor via the class object:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):

Now B.v would return some instance like <mymodule.VocalDescriptor object at 0xdeadbeef> which you can interact with. It is literally the descriptor object, defined as a class attribute, and its state B.v.__dict__ is shared between all instances of B.

Of course it is up to user’s code to define exactly what they want B.v to do, returning self is just the common pattern.

Barring any overrides, B.v is equivalent to type.__getattribute__(B, "v"), while b = B(); b.v is equivalent to object.__getattribute__(b, "v"). Both definitions invoke the __get__ method of the result if defined.

Note, thought, that the call to __get__ differs in each case. B.v passes None as the first argument, while B().v passes the instance itself. In both cases B is passed as the second argument.

B.v = 3, on the other hand, is equivalent to type.__setattr__(B, "v", 3), which does not invoke __set__.