Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 18 – Advanced Python: data model, metaprogramming and internals

Descriptors: how properties and methods work

Write Python descriptors with __get__, __set__ and __set_name__, tell data from non-data descriptors, and see how properties and methods are built on them.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A descriptor turns every use of a field into a method call · Data or non-data: which one beats the instance · A function binds itself to the instance it is read through · Practise: fields, kinds and bound methods. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Implement a descriptor with __get__, __set__ and __set_name__ that stores a value per instance
  • Distinguish data descriptors from non-data descriptors and predict which one beats the instance dictionary
  • Explain how a function stored in a class becomes a bound method through __get__
  • Build reusable validated fields for a model class

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement a descriptor with __get__, __set__ and __set_name__ that stores a value per instance
  • Distinguish data descriptors from non-data descriptors and predict which one beats the instance dictionary
  • Explain how a function stored in a class becomes a bound method through __get__
  • Build reusable validated fields for a model class

Before you start

On this page

qty = Positive() is one line in a class body, and from then on every assignment to qty, the one in __init__ included, is checked: setting it to 0 raises ValueError and keeps the old value. Positive is a descriptor, an object stored as a class attribute that decides what happens when that attribute is read, assigned or deleted through an instance. It does so by defining __get__, and optionally __set__ and __delete__: when the attribute lookup: What Python does to evaluate obj.name: the default __getattribute__ looks for a data descriptor on the class, then in the instance's own dictionary, then for anything else on the class, and only when all three fail is __getattr__ called. More in “Attribute lookup and the data model” of the previous lesson finds such an object on the class, it calls the method instead of handing the object back.

Properties work this way, and so do methods: a function stored in a class is a descriptor whose __get__ returns a bound method, which is where self comes from. A fourth method, __set_name__, tells a descriptor the name it was given when its class was created. Below you will build such fields, sort descriptors into two kinds, and bind a method by hand.

A descriptor turns every use of a field into a method call

fields.py declares three fields on an Order, builds two orders, and then tries three bad values on the first:

Positive and MaxLength fields that check every assignment Python · fields.py
class Field:
    """Keeps each instance's value in that instance, under the field's name."""

    def __set_name__(self, owner, name):     # called once, when the owning class is created
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:                      # read through the class: the descriptor itself
            return self
        try:
            return obj.__dict__[self.name]
        except KeyError:
            raise AttributeError(self.name) from None

    def __set__(self, obj, value):
        self.check(value)                    # refuse a bad value before storing it
        obj.__dict__[self.name] = value

    def check(self, value):
        pass


class Positive(Field):
    def check(self, value):
        if value <= 0:
            raise ValueError(f"{self.name} must be greater than 0, got {value}")


class MaxLength(Field):
    def __init__(self, limit):
        self.limit = limit

    def check(self, value):
        if len(value) > self.limit:
            raise ValueError(f"{self.name} has at most {self.limit} characters, got {len(value)}")


class Order:
    item = MaxLength(12)
    qty = Positive()
    price = Positive()

    def __init__(self, item, qty, price):
        self.item = item                     # each assignment runs a __set__
        self.qty = qty
        self.price = price


a = Order("chai", 3, 40)
b = Order("samosa", 2, 25)
print(a.qty * a.price, b.qty * b.price, vars(a))

for field, bad in (("qty", 0), ("price", -5), ("item", "masala dosa combo")):
    try:
        setattr(a, field, bad)
    except ValueError as e:
        print("ValueError:", e)

print(vars(a), type(Order.qty).__name__, Order.qty.name)

Output

120 50 {'item': 'chai', 'qty': 3, 'price': 40}
ValueError: qty must be greater than 0, got 0
ValueError: price must be greater than 0, got -5
ValueError: item has at most 12 characters, got 17
{'item': 'chai', 'qty': 3, 'price': 40} Positive qty

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fields.py

  • qty = Positive() and price = Positive() put two descriptor objects in the class. When Python creates Order, it calls __set_name__ on each with the name it was assigned to, so each object knows which field it is.
  • self.qty = qty in __init__ is an assignment through an instance. The class holds a data descriptor of that name, so Python calls its __set__ instead of storing the value: the check runs in the constructor too, and a refused value leaves the old one in place.
  • The values live in the instances. __set__ writes obj.__dict__["qty"] and __get__ reads it back, so each order keeps its own numbers. Using the field’s own name as the key is safe, because a data descriptor is consulted before the instance’s dictionary and the stored value can never hide it.
  • Read through the class, Order.qty calls __get__ with None for the instance, and the field returns itself, which lets tests and tools inspect it.

Follow those calls one message at a time. The middle column is the qty field, the one Positive object that Order holds for that name:

Interactive · message by message The calls behind one field of an order Step through fields.py one message at a time. The middle column is the qty field, the one Positive object that Order holds for that name.
The calls behind one field of an order, step 6 of 6. Messages so far: Your code to The qty field: __set_name__(Order, 'qty'); Your code to The qty field: __set__(a, 3); The qty field to vars(a): 'qty': 3; Your code to The qty field: __get__(a, Order); Your code to The qty field: __set__(a, 0); Your code to The qty field: __get__(None, Order).Your codeOrder and order aThe qty fieldname = 'qty'vars(a)qty is 3__set_name__(Order, 'qty')__set__(a, 3)'qty': 3__get__(a, Order)__set__(a, 0)__get__(None, Order)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The class is created. When the body of Order has run, type.__new__ calls __set_name__ on each field it finds. The qty field records the name it was assigned to.
  2. An assignment in __init__. Order('chai', 3, 40) runs self.qty = qty in __init__. The class holds a data descriptor called qty, so Python calls its __set__ instead of storing the value.
  3. Checked, then stored. 3 is greater than 0, so the check passes and __set__ writes the value into the order itself, under the field's name.
  4. A read. a.qty calls __get__(a, Order), which reads vars(a)['qty'] back. It gives 3, so a.qty * a.price is 120.
  5. A bad value. setattr(a, 'qty', 0) calls __set__(a, 0). The check raises ValueError before anything is stored, so vars(a) still holds 3.
  6. Through the class. Order.qty calls __get__(None, Order). With no instance to read from, the field returns itself, which is how the last line printed its class, Positive, and its name, qty.

The class holds one descriptor object per field, and every order keeps its own values in its own dictionary:

The class Order holds one Positive descriptor per field; its __get__ and __set__ read and write each order's own dictionary.class Orderqty = Positive()price = Positive()the Positive object for qtyname = 'qty', from __set_name__one object for every orderorder avars(a): {'qty': 3, ...}order bvars(b): {'qty': 2, ...}holds one per field__get__, __set____get__, __set__

The descriptor lives in the class, the values live in the instances

Text description of the diagram

The diagram has three levels, from top to bottom.

  • At the top is the class Order, whose body assigns qty = Positive() and price = Positive(). The class holds one descriptor object for each field.
  • Below it is the Positive object for qty. When the class was created, Python called its __set_name__ method, which recorded the name 'qty'.
  • At the bottom are two orders, a and b. Each has its own dictionary, with 'qty': 3 in a and 'qty': 2 in b.
  • Arrows lead from the descriptor to both orders: its __get__ method reads the value from the order it is called for, and its __set__ method checks a new value and writes it there. The same descriptor object serves every instance, which is why the values must not be stored on the descriptor itself.

Each line of code that touches the field becomes one call on its descriptor, here named d:

You write Python calls
class Order: finishes d.__set_name__(Order, "qty")
order.qty d.__get__(order, Order)
order.qty = 5 d.__set__(order, 5)
del order.qty d.__delete__(order), if defined
Order.qty d.__get__(None, Order)

Checkpoint

Check: The calls a field receives

  1. Question 1 of 2 What does Order("chai", 0, 40) do with the classes of fields.py?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises ValueError, because self.qty = 0 in __init__ calls the field's __set__

    self.qty = qty finds a data descriptor called qty on the class, so Python calls its __set__, which calls check() before storing. 0 is refused with ValueError: qty must be greater than 0, got 0, and the constructor stops.

  2. Question 2 of 2 Order.qty reads the field through the class, with no order at all. What does it return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The Positive object itself

    Order.qty calls __get__(None, Order). Field.__get__ checks for None and returns the descriptor, so tools and tests can reach the field object and its name.

Data or non-data: which one beats the instance

kinds.py asks each built-in kind which of the three methods its class defines:

Which descriptor methods the built-in kinds define Python · kinds.py
import functools


class Point:
    __slots__ = ("x", "y")


class Sample:
    def method(self): ...

    @classmethod
    def build(cls): ...

    @staticmethod
    def helper(): ...

    @property
    def size(self): ...

    @functools.cached_property
    def summary(self): ...


objects = {
    "function": Sample.__dict__["method"],
    "classmethod": Sample.__dict__["build"],
    "staticmethod": Sample.__dict__["helper"],
    "property": Sample.__dict__["size"],
    "cached_property": Sample.__dict__["summary"],
    "slot": Point.__dict__["x"],
}
print(f"{'stored in the class':20} {'__get__':8} {'__set__':8} {'__delete__':11} kind")
for label, obj in objects.items():
    has = [hasattr(type(obj), m) for m in ("__get__", "__set__", "__delete__")]
    kind = "data" if has[1] or has[2] else "non-data"
    print(f"{label:20} {str(has[0]):8} {str(has[1]):8} {str(has[2]):11} {kind}")

Output

stored in the class  __get__  __set__  __delete__  kind
function             True     False    False       non-data
classmethod          True     False    False       non-data
staticmethod         True     False    False       non-data
property             True     True     True        data
cached_property      True     False    False       non-data
slot                 True     True     True        data

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kinds.py

A descriptor that defines __set__ or __delete__ is a data descriptor: A descriptor that defines __set__ or __delete__, such as a property or a slot. Attribute lookup checks it before the instance's dictionary, so an instance attribute of the same name can never hide it., and it always beats an instance attribute of the same name. One that defines only __get__ is a non-data descriptor: A descriptor that defines only __get__, such as a function or functools.cached_property. An entry of the same name in the instance's dictionary hides it, which is how an instance attribute can shadow a method., and an entry in the instance’s dictionary hides it. The run shows the split: properties and the slots of __slots__ are data descriptors, while functions, class methods, static methods and functools.cached_property are not. Each choice is deliberate. A property must run its setter even if something wrote its name into the instance, while cached_property stores its result in the instance on purpose, so that the next read finds the value there and never calls the descriptor again.

Step through two descriptors that differ in one method, and watch what the same entry in the instance’s dictionary does to each:

Interactive · line by line The same entry in vars(a), against two kinds of descriptor own is vars(a). Both names get the value 5 there; watch what each read gives.

Program · who_wins.py

class Ten:
    def __get__(self, obj, owner):
        return 10


class Fixed(Ten):
    def __set__(self, obj, value):
        raise AttributeError("fixed")


class A:
    x = Ten()
    y = Fixed()


a = A()
own = vars(a)
own["x"] = 5
own["y"] = 5
x = a.x
y = a.y
a.x = 7

Variables

After line 22: Ten is Ten; Fixed is Fixed; A is A; a is <A object>; own is {'x': 7, 'y': 5}; x is 5; y is 10.TenTenFixedFixedAAa<A object>own'x':7'y':5x5y10
  • changed in this step

Printed so far

 

The program prints nothing.

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 11 steps are listed below.

Line run 22
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. class Ten: makes the class Ten. Its body runs only when it is used.
  3. Line 6. class Fixed(Ten): makes the class Fixed. Its body runs only when it is used. Fixed inherits __get__ from Ten and adds __set__. That one method makes it a data descriptor.
  4. Line 11. class A: makes the class A. Its body runs only when it is used.
  5. Line 16. a = A() sets a to <A object>.
  6. Line 17. own = vars(a) sets own to {}.
  7. Line 18. own["x"] = 5 adds the key 'x' to own, with the value 5.
  8. Line 19. own["y"] = 5 adds the key 'y' to own, with the value 5. Both names are now keys of own, written straight into the dictionary, past every descriptor.
  9. Line 20. x = a.x sets x to 5. Key point x is a non-data descriptor, so the instance's dictionary is searched before it, and a.x gives the 5 in own.
  10. Line 21. y = a.y sets y to 10. Key point y is a data descriptor, checked before the dictionary: a.y gives 10 from __get__, and the 5 in own is never read.
  11. Line 22. a.x = 7 a keeps its value, <A object>. That was the last line, so the program ends. a.x = 7 found no __set__ for x, so the value went into own. The same assignment to y would have called Fixed.__set__, which refuses.

Key idea

With __set__ or __delete__, a descriptor beats the instance’s dictionary, as every property does; with only __get__, the dictionary beats it, which is how an instance attribute can hide a method.

Checkpoint

Check: Data and non-data descriptors

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    class Upper:
        def __get__(self, obj, owner):
            return "UP"
    
        def __delete__(self, obj):
            pass
    
    
    class A:
        u = Upper()
    
    
    a = A()
    vars(a)["u"] = "low"
    print(a.u)
    Show the answer: Question 1 of 2

    Answer: UP

    A descriptor that defines __set__ or __delete__ is a data descriptor. Upper defines __delete__, so the lookup calls its __get__ before looking at vars(a), and the 'low' stored there is never read.

  2. Question 2 of 2 An instance attribute has the same name as one of these objects on its class. Which one can it hide?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A functools.cached_property

    Only non-data descriptors can be hidden: functions, class methods, static methods and cached_property in the run of kinds.py. Properties and slots are data descriptors, which the lookup checks before the instance.

A function binds itself to the instance it is read through

Methods use the same machinery. Step through a function bound by hand; each variable shows its parameters, so you can see self being filled in:

Interactive · line by line A function bound to an instance by its own __get__ Watch the parameters each variable shows. A bound method takes one fewer, because self is already filled in.

Program · bound_method.py

import types


class Greeter:
    def __init__(self, name):
        self.name = name

    def greet(self, other):
        return f"{self.name} greets {other}"


g = Greeter("Asha")
f = vars(Greeter)["greet"]
has = hasattr(f, "__get__"), hasattr(f, "__set__")
bound = f.__get__(g, Greeter)
same = bound.__self__ is g
print(bound("Ravi"), "|", g.greet("Ravi"))
plain = f.__get__(None, Greeter)
by_hand = types.MethodType(f, g)
print(plain is f, by_hand("Meena"))
own = vars(g)
g.greet = lambda other: f"hello {other}"
print(g.greet("Ravi"), "|", Greeter.greet(g, "Ravi"))

Variables

After line 23: Greeter is Greeter; g is <Greeter object>; f is greet(self, other); has is (True, False); bound is greet(other); same is True; plain is greet(self, other); by_hand is greet(other); own is {'name': 'Asha', 'greet': <lambda>(other)}.GreeterGreeterg<Greeter object>fgreet(self, other)hasTrue0False1boundgreet(other)sameTrueplaingreet(self, other)by_handgreet(other)own'name':'Asha''greet':<lambda>(other)
  • changed in this step

Printed so far

Asha greets Ravi | Asha greets Ravi
True Asha greets Meena
hello Ravi | Asha greets Ravi

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 15 steps are listed below.

Line run 23
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. import types loads types.
  3. Line 4. class Greeter: makes the class Greeter. Its body runs only when it is used.
  4. Line 12. g = Greeter("Asha") sets g to <Greeter object>.
  5. Line 13. f = vars(Greeter)["greet"] sets f to greet(self, other). The class stores a plain function, and its parameters are (self, other): nothing has bound it yet.
  6. Line 14. has = hasattr(f, "__get__"), hasattr(f, "__set__") makes has a tuple of 2 items. has is (True, False): the function has __get__ but no __set__, so it is a non-data descriptor.
  7. Line 15. bound = f.__get__(g, Greeter) sets bound to greet(other). Key point f.__get__(g, Greeter) is exactly what reading g.greet does. The result takes only other, because g is already its self.
  8. Line 16. same = bound.__self__ is g sets same to True.
  9. Line 17. print(bound("Ravi"), "|", g.greet("Ravi")) prints Asha greets Ravi | Asha greets Ravi.
  10. Line 18. plain = f.__get__(None, Greeter) sets plain to greet(self, other). Read through the class, __get__ receives None for the instance and returns the function itself, so plain is f.
  11. Line 19. by_hand = types.MethodType(f, g) sets by_hand to greet(other).
  12. Line 20. print(plain is f, by_hand("Meena")) prints True Asha greets Meena.
  13. Line 21. own = vars(g) sets own to {'name': 'Asha'}.
  14. Line 22. g.greet = lambda other: f"hello {other}" g keeps its value, <Greeter object>. Key point greet is now a key of own. A function is a non-data descriptor, so this entry hides the method for g, and for g only.
  15. Line 23. print(g.greet("Ravi"), "|", Greeter.greet(g, "Ravi")) prints hello Ravi | Asha greets Ravi. That was the last line, so the program ends.

  • The class stores a plain function, which has __get__ and no __set__: a non-data descriptor.
  • f.__get__(g, Greeter) returns a bound method whose __self__ is g, which is exactly what reading g.greet produces, and types.MethodType builds the same object by hand.
  • Read through the class, __get__ receives None and returns the function itself, which is why Greeter.greet(g, "Ravi") needs the instance passed in.
  • Because functions are non-data descriptors, assigning g.greet gives that one instance a callable of its own, which hides the method.

The same four methods explain the rest of the machinery. property is a data descriptor that can be rebuilt in Python in about thirty lines. classmethod binds its function to the class, and staticmethod does not bind at all. A descriptor that keeps values on itself shares one value between every instance of its class. The value can also live in the instance under a private name, or in a weakref.WeakKeyDictionary that the descriptor holds. __set_name__ runs only while a class is being created, so a descriptor attached later never learns its name.

Checkpoint

Check: Functions as descriptors

  1. Question 1 of 2 In bound_method.py, what does f.__get__(g, Greeter) give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A bound method whose __self__ is g and whose __func__ is f

    The trace showed f with the parameters (self, other) and the bound method with (other) only: g is stored in the bound method's __self__, and calling it passes g in as self.

  2. Question 2 of 2 After g.greet = lambda other: f"hello {other}", what does Greeter.greet(g, "Ravi") return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 'Asha greets Ravi'

    The instance attribute hides the method for reads through g only. Greeter.greet finds the function on the class, and calling it with g and 'Ravi' gives 'Asha greets Ravi', as the last line of the example printed.

Python Online Compiler Delete the __set__ method of Field, write vars(a)['qty'] = -1 and read a.qty: which one wins now, and why?

Practise: fields, kinds and bound methods

These questions cover the three ideas above, the one you have missed most first, and say why each answer is right or wrong as soon as you choose it.

Practice that adapts to you

Practise this lesson

7 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 When does Python call a descriptor's __set_name__?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: Once, while the class whose body assigns the descriptor is being created

    type.__new__ calls __set_name__(owner, name) on every attribute of the new class that has the method. A descriptor attached to the class later is not told its name unless you call the method yourself.

  2. Question 2 of 7 After a = Order("chai", 3, 40) in fields.py, where is the quantity 3 stored?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: In vars(a), under the key qty

    The descriptor lives in the class and the values live in the instances: vars(a) printed {'item': 'chai', 'qty': 3, 'price': 40}. Using the field's own name as the key is safe because a data descriptor is checked first.

  3. Question 3 of 7 A teammate deletes __set__ from Field and keeps __get__ and check(). What does a.qty = 0 do now?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: It stores 0 in vars(a) without any check, and a.qty then gives 0

    __set__ is what made the field a data descriptor and what ran the check. Without it, assignment finds no data descriptor and stores in vars(a), and the read finds the stored value before the descriptor is asked.

  4. Question 4 of 7 What does this print?

    Read the code, then choose one answer.

    class Ten:
        def __get__(self, obj, objtype=None):
            return 10
    
    class A:
        x = Ten()
    
    a = A()
    before = (a.x, A.x)
    a.x = 5
    print(before, a.x)
    Show the answer: Question 4 of 7

    Answer: (10, 10) 5

    Ten defines only __get__, so it is a non-data descriptor. Both reads call it, and this __get__ returns 10 even when it is read through the class. a.x = 5 finds no __set__, so the value is stored in the instance, and from then on the instance's dictionary wins over the non-data descriptor.

  5. Question 5 of 7 Which of these are data descriptors, so that an entry in the instance's dictionary can never hide them? Choose every right answer.

    Choose every answer that is right.

    Show the answer: Question 5 of 7

    Answer:

    • A slot created by __slots__
    • A property

    A property and a slot's member descriptor define __set__ and __delete__. Functions, class methods, static methods and cached_property define only __get__, which is exactly what lets cached_property store its result in the instance and be skipped on the next read.

  6. Question 6 of 7 What is the difference between Greeter.greet and g.greet?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: Greeter.greet is the plain function; g.greet is a bound method holding the function and g

    The function's __get__ decides: with an instance it returns a bound method whose __self__ is that instance, and with None, as for a read through the class, it returns the function, which then needs the instance passed in.

  7. Question 7 of 7 A class body says handler = print_line, where print_line(prefix, text) is an ordinary function defined outside the class. Code calls obj.handler("hi"). What does print_line receive?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: obj as prefix and 'hi' as text

    What makes a function a method is not where it was defined but how it is read. A function on a class, read through an instance, comes back bound, so obj.handler('hi') calls print_line(obj, 'hi'). Wrap it in staticmethod to store a function that should not bind.

property rebuilt and checked against the built-in, then the other kinds opened up Pro: opens with a Pro, Premium or Ultimate pass

Each built-in descriptor is a few lines of Python once the four methods are clear. This part rebuilds property and checks it against the built-in line by line, rebuilds classmethod and staticmethod, opens up what __slots__ and cached_property put in a class, and records the arguments of every way a descriptor is called.

In this part:

  • property in thirty lines, compared with the built-in
  • Class methods, static methods, slots and cached properties, opened up
  • Three ways a descriptor is called: instance, class and super()

1 diagram · 3 runnable examples (Python) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Fields that hold up: the shared-value bug, four storage choices counted, and an exercise Pro: opens with a Pro, Premium or Ultimate pass

One descriptor object serves every instance of its class, so where it keeps per-instance values decides whether a field works at all. This part reproduces the shared-value bug, counts what four storage choices leave behind, shows two traps in the timing of __set_name__, and ends with an exercise that builds a small model layer.

In this part:

  • One value for every order: the shared-value bug, reproduced
  • Four places to keep the value, and what each leaves behind, counted
  • A field’s name is set once, at class creation: two traps reproduced
  • Error messages that name the right class, and rules that span two fields
  • Exercise: Integer, String and Choice fields for a model class
    • Integer, String and Choice fields for a small model class

1 diagram · 3 runnable examples (Python) · 1 exercise with sample tests · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between a data descriptor and a non-data descriptor?

    Both are objects stored as class attributes with a __get__ method, which attribute lookup calls instead of returning the object. A data descriptor also defines __set__ or __delete__, and it takes priority over the instance's own dictionary for reads and writes: a property is the usual example, which is why an instance attribute can never hide a property. A non-data descriptor defines only __get__, and an entry of the same name in the instance's dictionary hides it. Functions are non-data descriptors, which is why an instance attribute can shadow a method, and functools.cached_property relies on the same rule: its first read stores the result in the instance's dictionary, and later reads find it there without calling the descriptor again.

  2. Fresher to mid

    A validated-field descriptor passes its unit tests, but in production two customers' orders show the same quantity. What is the likely bug, how do you confirm it in a debugger, and where should the descriptor keep each order's value?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Twelve model classes have 40 properties between them, and almost all check "a positive number" or "a string of at most N characters". A teammate proposes descriptor classes instead. Make the case for and against, and say what you would do.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    Design the column attribute of a small ORM so that User.email gives an object you can build queries with, while user.email gives that user's value. Which descriptor methods do you write, what does each receive, and what breaks with __slots__ or with a subclass that redefines the column?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A descriptor is a class attribute with __get__, and optionally __set__ and __delete__; the attribute lookup calls these methods instead of returning the object.
  • With __set__ or __delete__ it is a data descriptor and beats the instance’s dictionary; with only __get__ the instance wins.
  • Functions are non-data descriptors whose __get__ returns bound methods; properties are data descriptors.
  • __set_name__ gives each field its name while the class is created, so one field class serves many attributes.
  • Keep per-instance values in the instance, never on the descriptor, which every instance shares.

Terms in this lesson

data descriptor
A descriptor that defines __set__ or __delete__, such as a property or a slot. Attribute lookup checks it before the instance's dictionary, so an instance attribute of the same name can never hide it.
descriptor
An object stored as a class attribute whose class defines __get__, and optionally __set__ or __delete__. Attribute lookup calls these methods instead of returning the object; properties, methods and __slots__ all work this way.
non-data descriptor
A descriptor that defines only __get__, such as a function or functools.cached_property. An entry of the same name in the instance's dictionary hides it, which is how an instance attribute can shadow a method.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.