When building frameworks, ORMs, or validation systems, you need fine-grained control over attribute access. Descriptors provide the low-level mechanism that powers @property, class methods, and ORM field definitions, letting you intercept and customize get, set, and delete operations.

Descriptors are a low-level mechanism for customizing attribute access. A descriptor is any object that implements __get__(), __set__(), or __delete__(). Properties are actually implemented using the descriptor protocol.

The Descriptor Protocol

descriptor_protocol.py
Replay: real traced execution (multi-file project)
"""Basic descriptor protocol"""

# Simple descriptor
print("Simple descriptor:")

class Descriptor:
    def __get__(self, obj, type=None):
        print(f"  __get__ called: obj={obj}, type={type}")
        return 42

    def __set__(self, obj, value):
        print(f"  __set__ called: obj={obj}, value={value}")

class MyClass:
    attr = Descriptor()

obj = MyClass()

# Getting attribute
print("Getting attribute:")
value = obj.attr
print(f"value={value}")

# Setting attribute
print("\nSetting attribute:")
obj.attr = 100

# Storing values
print("\nStoring values:")

class ValueDescriptor:
    def __init__(self):
        self.value = None

    def __get__(self, obj, type=None):
        print(f"  Getting value: {self.value}")
        return self.value

    def __set__(self, obj, value):
        print(f"  Setting value: {value}")
        self.value = value

class Container:
    data = ValueDescriptor()

c = Container()
c.data = 123
print(f"Retrieved: {c.data}")

# Multiple instances problem
print("\nMultiple instances problem:")

# Problem: shared descriptor instance
c1 = Container()
c2 = Container()

c1.data = "first"
c2.data = "second"

print(f"c1.data = {c1.data}")  # Will be "second"!
print(f"c2.data = {c2.data}")
print("  Note: Both share same descriptor instance")

# Proper storage
print("\nProper storage:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value

class Person:
    name = ProperDescriptor('name')
    age = ProperDescriptor('age')

p1 = Person()
p2 = Person()

p1.name = "Alice"
p1.age = 30

p2.name = "Bob"
p2.age = 25

print(f"p1: {p1.name}, {p1.age}")
print(f"p2: {p2.name}, {p2.age}")

# Class vs instance access
print("\nClass vs instance access:")

class SmartDescriptor:
    def __get__(self, obj, type=None):
        if obj is None:
            return f"Descriptor accessed from class {type.__name__}"
        return f"Descriptor accessed from instance of {type.__name__}"

class MyClass:
    attr = SmartDescriptor()

# Access from class
print(f"MyClass.attr: {MyClass.attr}")

# Access from instance
obj = MyClass()
print(f"obj.attr: {obj.attr}")

# Read-only descriptor
print("\nRead-only descriptor:")

class ReadOnlyDescriptor:
    def __init__(self, value):
        self.value = value

    def __get__(self, obj, type=None):
        return self.value

    def __set__(self, obj, value):
        raise AttributeError("Cannot modify read-only attribute")

class Config:
    VERSION = ReadOnlyDescriptor("1.0.0")

config = Config()
print(f"Version: {config.VERSION}")

try:
    config.VERSION = "2.0.0"
except AttributeError as e:
    print(f"Error: {e}")

# Descriptor with state
print("\nDescriptor with state:")

class CountedDescriptor:
    def __init__(self, name):
        self.name = name
        self.access_count = 0

    def __get__(self, obj, type=None):
        self.access_count += 1
        print(f"  Access #{self.access_count}")
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value

class TrackedClass:
    value = CountedDescriptor('value')

tracked = TrackedClass()
tracked.value = 100

print("Accessing value multiple times:")
print(tracked.value)
print(tracked.value)
print(tracked.value)

# Delete support
print("\nDelete support:")

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

    def __get__(self, obj, type=None):
        return obj.__dict__.get(self.name, "Not set")

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value

    def __delete__(self, obj):
        print(f"  Deleting {self.name}")
        if self.name in obj.__dict__:
            del obj.__dict__[self.name]

class MyClass:
    attr = DeletableDescriptor('attr')

obj = MyClass()
obj.attr = "value"
print(f"attr = {obj.attr}")

del obj.attr
print(f"After delete: {obj.attr}")

# Practical example
print("\nPractical example:")

class TypedDescriptor:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} must be {self.expected_type.__name__}")
        obj.__dict__[self.name] = value

class User:
    name = TypedDescriptor('name', str)
    age = TypedDescriptor('age', int)
    email = TypedDescriptor('email', str)

user = User()
user.name = "Alice"
user.age = 30
user.email = "alice@example.com"

print(f"User: {user.name}, {user.age}, {user.email}")

try:
    user.age = "thirty"
except TypeError as e:
    print(f"Error: {e}")

  1. attr ← (empty), obj ← ⟨MyClass A⟩

    1"""Basic descriptor protocol"""23# Simple descriptor4print("Simple descriptor:")56class Descriptor:7    def __get__(self, obj, type=None):8        print(f"  __get__ called: obj={obj}, type={type}")9        return 4210    11    def __set__(self, obj, value):12        print(f"  __set__ called: obj={obj}, value={value}")1314class MyClass:15    attr→ (empty) = Descriptor()1617obj→ ⟨MyClass A⟩ = MyClass()1819# Getting attribute20print("Getting attribute:")21value = obj.attr4222print(f"value={value}")
    outputSimple descriptor:
    Getting attribute:
  2. def __get__(self, obj, type=None):

    6class Descriptor:7    def __get__(self⟨Descriptor B⟩, obj⟨MyClass A⟩, type<class '__main__.MyClass'>=NoneNone):8        print(f"  __get__ called: obj={obj⟨MyClass A⟩}, type={type}")9        return 42
    output  __get__ called: obj=⟨MyClass A⟩, type=<class '__main__.MyClass'>
  3. value ← 42

    20print("Getting attribute:")21value→ 42 = obj.attr4222print(f"value={value42}")2324# Setting attribute25print("\nSetting attribute:")26obj.attr = 100
    outputvalue=42
    
    Setting attribute:
  4. def __set__(self, obj, value):

    11def __set__(self⟨Descriptor B⟩, obj⟨MyClass A⟩, value100):12    print(f"  __set__ called: obj={obj⟨MyClass A⟩}, value={value100}")
    output  __set__ called: obj=⟨MyClass A⟩, value=100
  5. obj.attr ← 42

    25print("\nSetting attribute:")26obj.attr→ 42 = 1002728# Storing values29print("\nStoring values:")3031class ValueDescriptor:32    def __init__(self):33        self.value = None34    35    def __get__(self, obj, type=None):36        print(f"  Getting value: {self.value}")37        return self.value38    39    def __set__(self, obj, value):40        print(f"  Setting value: {value}")41        self.value = value4243class Container:44    data = ValueDescriptor()
    output
    Storing values:
  6. self.value ← None

    31class ValueDescriptor:32    def __init__(self⟨ValueDescriptor C⟩):33        self.value→ None = None
  7. data ← (empty), c ← ⟨Container D⟩

    43class Container:44    data→ (empty) = ValueDescriptor()4546c→ ⟨Container D⟩ = Container()47c.data = 12348print(f"Retrieved: {c.data}")
  8. self.value ← 123

    pass 1 of 3
    39def __set__(self⟨ValueDescriptor C⟩, obj⟨Container D⟩, value123):40    print(f"  Setting value: {value123}")41    self.value→ 123 = value123
    output  Setting value: 123
    All 3 passes — pass 1 is the card above
    passobjvalueself.value
    1⟨Container D⟩123123
    2⟨Container E⟩firstfirst
    3⟨Container F⟩secondsecond
  9. c.data ← 123

    46c = Container()47c.data→ 123 = 12348print(f"Retrieved: {c.data123}")
  10. def __get__(self, obj, type=None):

    pass 1 of 3
    35def __get__(self⟨ValueDescriptor C⟩, obj⟨Container D⟩, type<class '__main__.Container'>=NoneNone):36    print(f"  Getting value: {self.value123}")37    return self.value123
    output  Getting value: 123
    All 3 passes — pass 1 is the card above
    passobjself.value
    1⟨Container D⟩123
    2⟨Container E⟩second
    3⟨Container F⟩second
  11. c1 ← ⟨Container E⟩, c2 ← ⟨Container F⟩

    47c.data = 12348print(f"Retrieved: {c.data123}")4950# Multiple instances problem51print("\nMultiple instances problem:")5253# Problem: shared descriptor instance54c1→ ⟨Container E⟩ = Container()55c2→ ⟨Container F⟩ = Container()5657c1.data = "first"58c2.data = "second"
    outputRetrieved: 123
    
    Multiple instances problem:
  12. c1.data ← first

    57c1.data→ first = "first"58c2.data = "second"
  13. c2.data ← second

    57c1.data = "first"58c2.data→ second = "second"5960print(f"c1.data = {c1.datasecond}")  # Will be "second"!61print(f"c2.data = {c2.data}")
  14. print(f"c1.data = {c1.data}") # Will be "second"!

    60print(f"c1.data = {c1.datasecond}")  # Will be "second"!61print(f"c2.data = {c2.datasecond}")62print("  Note: Both share same descriptor instance")
    outputc1.data = second
  15. print(f"c2.data = {c2.data}")

    60print(f"c1.data = {c1.data}")  # Will be "second"!61print(f"c2.data = {c2.datasecond}")62print("  Note: Both share same descriptor instance")6364# Proper storage65print("\nProper storage:")6667class ProperDescriptor:68    def __init__(self, name):69        self.name = name70    71    def __get__(self, obj, type=None):72        if obj is None:73            return self74        return obj.__dict__.get(self.name)75    76    def __set__(self, obj, value):77        obj.__dict__[self.name] = value7879class Person:80    name = ProperDescriptor('name')81    age = ProperDescriptor('age')
    outputc2.data = second
      Note: Both share same descriptor instance
    
    Proper storage:
  16. self.name ← name

    pass 1 of 2
    67class ProperDescriptor:68    def __init__(self⟨ProperDescriptor G⟩, namename):69        self.name→ name = namename
  17. name ← (empty)

    79class Person:80    name→ (empty) = ProperDescriptor('name')81    age = ProperDescriptor('age')
  18. self.name ← age

    pass 2 of 2
    67class ProperDescriptor:68    def __init__(self⟨ProperDescriptor H⟩, nameage):69        self.name→ age = nameage
  19. age ← (empty), p1 ← ⟨Person I⟩, p2 ← ⟨Person J⟩

    80    name = ProperDescriptor('name')81    age→ (empty) = ProperDescriptor('age')8283p1→ ⟨Person I⟩ = Person()84p2→ ⟨Person J⟩ = Person()8586p1.name = "Alice"87p1.age = 30
  20. obj.__dict__[self.name] ← Alice

    pass 1 of 4
    76def __set__(self⟨ProperDescriptor G⟩, obj⟨Person I⟩, valueAlice):77    obj.__dict__[self.name]→ Alice = valueAlice
    All 4 passes — pass 1 is the card above
    passselfobjvalueobj.__dict__[self.name]
    1⟨ProperDescriptor G⟩⟨Person I⟩AliceAlice
    2⟨ProperDescriptor H⟩⟨Person I⟩3030
    3⟨ProperDescriptor G⟩⟨Person J⟩BobBob
    4⟨ProperDescriptor H⟩⟨Person J⟩2525
  21. p1.name ← Alice

    86p1.name→ Alice = "Alice"87p1.age = 30
  22. p1.age ← 30

    86p1.name = "Alice"87p1.age→ 30 = 308889p2.name = "Bob"90p2.age = 25
  23. p2.name ← Bob

    89p2.name→ Bob = "Bob"90p2.age = 25
  24. p2.age ← 25

    89p2.name = "Bob"90p2.age→ 25 = 259192print(f"p1: {p1.nameAlice}, {p1.age30}")93print(f"p2: {p2.name}, {p2.age}")
  25. def __get__(self, obj, type=None):

    pass 1 of 4
    71def __get__(self⟨ProperDescriptor G⟩, obj⟨Person I⟩, type<class '__main__.Person'>=NoneNone):72    if obj is None:73        return self74    return obj.__dict__{'name': 'Alice', 'age': 30}.get(self.namename)
    All 4 passes — pass 1 is the card above
    passselfobjobj.__dict__self.name
    1⟨ProperDescriptor G⟩⟨Person I⟩{'name': 'Alice', 'age': 30}name
    2⟨ProperDescriptor H⟩⟨Person I⟩{'name': 'Alice', 'age': 30}age
    3⟨ProperDescriptor G⟩⟨Person J⟩{'name': 'Bob', 'age': 25}name
    4⟨ProperDescriptor H⟩⟨Person J⟩{'name': 'Bob', 'age': 25}age
  26. print(f"p1: {p1.name}, {p1.age}")

    92print(f"p1: {p1.nameAlice}, {p1.age30}")93print(f"p2: {p2.nameBob}, {p2.age25}")
    outputp1: Alice, 30
  27. attr ← (empty)

    92print(f"p1: {p1.name}, {p1.age}")93print(f"p2: {p2.nameBob}, {p2.age25}")9495# Class vs instance access96print("\nClass vs instance access:")9798class SmartDescriptor:99    def __get__(self, obj, type=None):100        if obj is None:101            return f"Descriptor accessed from class {type.__name__}"102        return f"Descriptor accessed from instance of {type.__name__}"103104class MyClass:105    attr→ (empty) = SmartDescriptor()106107# Access from class108print(f"MyClass.attr: {MyClass.attrDescriptor accessed from class MyClass}")
    outputp2: Bob, 25
    
    Class vs instance access:
  28. def __get__(self, obj, type=None):

    pass 1 of 2
    98class SmartDescriptor:99    def __get__(self⟨SmartDescriptor K⟩, objNone, type<class '__main__.MyClass'>=NoneNone):100        if obj is None:101            return f"Descriptor accessed from class {type.__name__}"
  29. if obj is None:

    99def __get__(self, obj, type=None):100    if objNone is None:101        return f"Descriptor accessed from class {type.__name__MyClass}"102    return f"Descriptor accessed from instance of {type.__name__}"
  30. obj ← ⟨MyClass L⟩

    107# Access from class108print(f"MyClass.attr: {MyClass.attrDescriptor accessed from class MyClass}")109110# Access from instance111obj→ ⟨MyClass L⟩ = MyClass()112print(f"obj.attr: {obj.attrDescriptor accessed from instance of MyClass}")
    outputMyClass.attr: Descriptor accessed from class MyClass
  31. def __get__(self, obj, type=None):

    pass 2 of 2
    98class SmartDescriptor:99    def __get__(self⟨SmartDescriptor K⟩, obj⟨MyClass L⟩, type<class '__main__.MyClass'>=NoneNone):100        if obj is None:101            return f"Descriptor accessed from class {type.__name__}"102        return f"Descriptor accessed from instance of {type.__name__MyClass}"
  32. print(f"obj.attr: {obj.attr}")

    111obj = MyClass()112print(f"obj.attr: {obj.attrDescriptor accessed from instance of MyClass}")113114# Read-only descriptor115print("\nRead-only descriptor:")116117class ReadOnlyDescriptor:118    def __init__(self, value):119        self.value = value120    121    def __get__(self, obj, type=None):122        return self.value123    124    def __set__(self, obj, value):125        raise AttributeError("Cannot modify read-only attribute")126127class Config:128    VERSION = ReadOnlyDescriptor("1.0.0")
    outputobj.attr: Descriptor accessed from instance of MyClass
    
    Read-only descriptor:
  33. self.value ← 1.0.0

    117class ReadOnlyDescriptor:118    def __init__(self⟨ReadOnlyDescriptor A⟩, value1.0.0):119        self.value→ 1.0.0 = value1.0.0
  34. VERSION ← (empty), config ← ⟨Config M⟩

    127class Config:128    VERSION→ (empty) = ReadOnlyDescriptor("1.0.0")129130config→ ⟨Config M⟩ = Config()131print(f"Version: {config.VERSION1.0.0}")
  35. def __get__(self, obj, type=None):

    121def __get__(self⟨ReadOnlyDescriptor A⟩, obj⟨Config M⟩, type<class '__main__.Config'>=NoneNone):122    return self.value1.0.0
  36. print(f"Version: {config.VERSION}")

    130config = Config()131print(f"Version: {config.VERSION1.0.0}")
    outputVersion: 1.0.0
  37. def __set__(self, obj, value):

    124def __set__(self⟨ReadOnlyDescriptor A⟩, obj⟨Config M⟩, value2.0.0):125    raise AttributeError("Cannot modify read-only attribute")
  38. except AttributeError as e:

    134    config.VERSION = "2.0.0"135except AttributeError as e:136    print(f"Error: {eCannot modify read-only attribute}")
    outputError: Cannot modify read-only attribute
  39. print(" Descriptor with state:")

    138# Descriptor with state139print("\nDescriptor with state:")140141class CountedDescriptor:142    def __init__(self, name):143        self.name = name144        self.access_count = 0145    146    def __get__(self, obj, type=None):147        self.access_count += 1148        print(f"  Access #{self.access_count}")149        return obj.__dict__.get(self.name)150    151    def __set__(self, obj, value):152        obj.__dict__[self.name] = value153154class TrackedClass:155    value = CountedDescriptor('value')
    output
    Descriptor with state:
  40. self.name ← value, self.access_count ← 0

    141class CountedDescriptor:142    def __init__(self⟨CountedDescriptor N⟩, namevalue):143        self.name→ value = namevalue144        self.access_count→ 0 = 0
  41. value ← 42, tracked ← ⟨TrackedClass O⟩

    154class TrackedClass:155    value→ 42 = CountedDescriptor('value')156157tracked→ ⟨TrackedClass O⟩ = TrackedClass()158tracked.value = 100
  42. obj.__dict__[self.name] ← 100

    151def __set__(self⟨CountedDescriptor N⟩, obj⟨TrackedClass O⟩, value100):152    obj.__dict__[self.name]→ 100 = value100
  43. tracked.value ← 100

    157tracked = TrackedClass()158tracked.value→ 100 = 100159160print("Accessing value multiple times:")161print(tracked.value100)162print(tracked.value)
    outputAccessing value multiple times:
  44. self.access_count ← 3

    pass 1 of 3
    146def __get__(self⟨CountedDescriptor N⟩, obj⟨TrackedClass O⟩, type<class '__main__.TrackedClass'>=NoneNone):147    self.access_count→ 3 += 1148    print(f"  Access #{self.access_count3}")149    return obj.__dict__{'value': 100}.get(self.namevalue)
    output  Access #3
    All 3 passes — pass 1 is the card above
    passself.access_count
    12 3
    25 6
    38 9
  45. print(tracked.value)

    160print("Accessing value multiple times:")161print(tracked.value100)162print(tracked.value100)163print(tracked.value)
    output100
  46. print(tracked.value)

    161print(tracked.value)162print(tracked.value100)163print(tracked.value100)
    output100
  47. print(tracked.value)

    162print(tracked.value)163print(tracked.value100)164165# Delete support166print("\nDelete support:")167168class DeletableDescriptor:169    def __init__(self, name):170        self.name = name171    172    def __get__(self, obj, type=None):173        return obj.__dict__.get(self.name, "Not set")174    175    def __set__(self, obj, value):176        obj.__dict__[self.name] = value177    178    def __delete__(self, obj):179        print(f"  Deleting {self.name}")180        if self.name in obj.__dict__:181            del obj.__dict__[self.name]182183class MyClass:184    attr = DeletableDescriptor('attr')
    output100
    
    Delete support:
  48. self.name ← attr

    168class DeletableDescriptor:169    def __init__(self⟨DeletableDescriptor P⟩, nameattr):170        self.name→ attr = nameattr
  49. attr ← (empty), obj ← ⟨MyClass Q⟩

    183class MyClass:184    attr→ (empty) = DeletableDescriptor('attr')185186obj→ ⟨MyClass Q⟩ = MyClass()187obj.attr = "value"188print(f"attr = {obj.attr}")
  50. obj.__dict__[self.name] ← value

    175def __set__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩, valuevalue):176    obj.__dict__[self.name]→ value = valuevalue
  51. obj.attr ← value

    186obj = MyClass()187obj.attr→ value = "value"188print(f"attr = {obj.attrvalue}")
  52. def __get__(self, obj, type=None):

    pass 1 of 2
    172def __get__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩, type<class '__main__.MyClass'>=NoneNone):173    return obj.__dict__{'attr': 'value'}.get(self.nameattr, "Not set")
  53. print(f"attr = {obj.attr}")

    187obj.attr = "value"188print(f"attr = {obj.attrvalue}")
    outputattr = value
  54. def __delete__(self, obj):

    178def __delete__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩):179    print(f"  Deleting {self.nameattr}")180    if self.name in obj.__dict__:
    output  Deleting attr
  55. obj.__dict__[self.name] ← (empty)

    179print(f"  Deleting {self.name}")180if self.nameattr in obj.__dict__{'attr': 'value'}:181    del obj.__dict__[self.name]→ (empty)
  56. print(f"After delete: {obj.attr}")

    190del obj.attr191print(f"After delete: {obj.attrNot set}")
  57. def __get__(self, obj, type=None):

    pass 2 of 2
    172def __get__(self⟨DeletableDescriptor P⟩, obj⟨MyClass Q⟩, type<class '__main__.MyClass'>=NoneNone):173    return obj.__dict__{}.get(self.nameattr, "Not set")
  58. print(f"After delete: {obj.attr}")

    190del obj.attr191print(f"After delete: {obj.attrNot set}")192193# Practical example194print("\nPractical example:")195196class TypedDescriptor:197    def __init__(self, name, expected_type):198        self.name = name199        self.expected_type = expected_type200    201    def __get__(self, obj, type=None):202        if obj is None:203            return self204        return obj.__dict__.get(self.name)205    206    def __set__(self, obj, value):207        if not isinstance(value, self.expected_type):208            raise TypeError(f"{self.name} must be {self.expected_type.__name__}")209        obj.__dict__[self.name] = value210211class User:212    name = TypedDescriptor('name', str)213    age = TypedDescriptor('age', int)
    outputAfter delete: Not set
    
    Practical example:
  59. self.name ← name, self.expected_type ← <class 'str'>

    pass 1 of 3
    196class TypedDescriptor:197    def __init__(self⟨TypedDescriptor L⟩, namename, expected_type<class 'str'>):198        self.name→ name = namename199        self.expected_type→ <class 'str'> = expected_type<class 'str'>
    All 3 passes — pass 1 is the card above
    passselfnameexpected_typeself.nameself.expected_type
    1⟨TypedDescriptor L⟩name<class 'str'>name<class 'str'>
    2⟨TypedDescriptor R⟩age<class 'int'>age<class 'int'>
    3⟨TypedDescriptor S⟩email<class 'str'>email<class 'str'>
  60. name ← (empty)

    211class User:212    name→ (empty) = TypedDescriptor('name', str)213    age = TypedDescriptor('age', int)214    email = TypedDescriptor('email', str)
  61. age ← (empty)

    212name = TypedDescriptor('name', str)213age→ (empty) = TypedDescriptor('age', int)214email = TypedDescriptor('email', str)
  62. email ← (empty), user ← ⟨User T⟩

    213    age = TypedDescriptor('age', int)214    email→ (empty) = TypedDescriptor('email', str)215216user→ ⟨User T⟩ = User()217user.name = "Alice"218user.age = 30
  63. obj.__dict__[self.name] ← Alice

    pass 1 of 4
    206def __set__(self⟨TypedDescriptor L⟩, obj⟨User T⟩, valueAlice):207    if not isinstance(value, self.expected_type):208        raise TypeError(f"{self.name} must be {self.expected_type.__name__}")209    obj.__dict__[self.name]→ Alice = valueAlice
    All 4 passes — pass 1 is the card above
    passselfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]
    1⟨TypedDescriptor L⟩AliceAlice
    2⟨TypedDescriptor R⟩3030
    3⟨TypedDescriptor S⟩alice@example.comalice@example.com
    4⟨TypedDescriptor R⟩thirty<class 'int'>ageintage must be int
  64. user.name ← Alice

    216user = User()217user.name→ Alice = "Alice"218user.age = 30219user.email = "alice@example.com"
  65. user.age ← 30

    217user.name = "Alice"218user.age→ 30 = 30219user.email = "alice@example.com"
  66. user.email ← alice@example.com

    218user.age = 30219user.email→ alice@example.com = "alice@example.com"220221print(f"User: {user.nameAlice}, {user.age30}, {user.emailalice@example.com}")
  67. def __get__(self, obj, type=None):

    pass 1 of 3
    201def __get__(self⟨TypedDescriptor L⟩, obj⟨User T⟩, type<class '__main__.User'>=NoneNone):202    if obj is None:203        return self204    return obj.__dict__{'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}.get(self.namename)
    All 3 passes — pass 1 is the card above
    passselfself.name
    1⟨TypedDescriptor L⟩name
    2⟨TypedDescriptor R⟩age
    3⟨TypedDescriptor S⟩email
  68. print(f"User: {user.name}, {user.age}, {user.email}")

    221print(f"User: {user.nameAlice}, {user.age30}, {user.emailalice@example.com}")
    outputUser: Alice, 30, alice@example.com
  69. if not isinstance(value, self.expected_type):

    206def __set__(self, obj, value):207    if not isinstance(valuethirty, self.expected_type<class 'int'>):208        raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}")209    obj.__dict__[self.name] = value
  70. except TypeError as e:

    224    user.age = "thirty"225except TypeError as e:226    print(f"Error: {eage must be int}")
    outputError: age must be int

The key insight: descriptors are defined on the class but called when accessing attributes on instances. The obj parameter tells you which instance is being accessed.

descriptor protocol Implementing `__get__`, `__set__`, and/or `__delete__` methods to intercept attribute access on a class.

Property as a Descriptor

descriptor_property.py
Replay: real traced execution (multi-file project)
"""Property as a descriptor"""

# Property implementation
print("Property implementation:")

# This is roughly how @property works
class PropertyDescriptor:
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    def get_celsius(self):
        return self._celsius

    def set_celsius(self, value):
        self._celsius = value

    celsius = PropertyDescriptor(get_celsius, set_celsius)

temp = Temperature(25)
print(f"Celsius: {temp.celsius}")
temp.celsius = 30
print(f"Updated: {temp.celsius}")

# Standard @property
print("\nStandard @property:")

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """Get radius"""
        return self._radius

    @radius.setter
    def radius(self, value):
        """Set radius"""
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        """Computed area"""
        import math
        return math.pi * self._radius ** 2

circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Area: {circle.area:.2f}")

circle.radius = 10
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area:.2f}")

# Read-only property
print("\nRead-only property:")

class Person:
    def __init__(self, first_name, last_name):
        self._first_name = first_name
        self._last_name = last_name

    @property
    def full_name(self):
        """Read-only computed property"""
        return f"{self._first_name} {self._last_name}"

person = Person("Alice", "Smith")
print(f"Full name: {person.full_name}")

try:
    person.full_name = "Bob Jones"
except AttributeError as e:
    print(f"Error: {e}")

# Cached property
print("\nCached property:")

class CachedProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, type=None):
        if obj is None:
            return self

        # Check if cached
        cache_name = f'_cached_{self.name}'
        if cache_name not in obj.__dict__:
            # Compute and cache
            print(f"  Computing {self.name}...")
            obj.__dict__[cache_name] = self.func(obj)

        return obj.__dict__[cache_name]

class DataProcessor:
    def __init__(self, data):
        self.data = data

    @CachedProperty
    def expensive_result(self):
        # Expensive computation
        return sum(x ** 2 for x in self.data)

processor = DataProcessor([1, 2, 3, 4, 5])

print("First access:")
result1 = processor.expensive_result
print(f"Result: {result1}")

print("\nSecond access (cached):")
result2 = processor.expensive_result
print(f"Result: {result2}")

# Validated property
print("\nValidated property:")

class ValidatedProperty:
    def __init__(self, name, validator):
        self.name = name
        self.validator = validator

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not self.validator(value):
            raise ValueError(f"Invalid value for {self.name}: {value}")
        obj.__dict__[self.name] = value

def is_positive(value):
    return value > 0

def is_valid_email(value):
    return "@" in value and "." in value

class Account:
    balance = ValidatedProperty('balance', is_positive)
    email = ValidatedProperty('email', is_valid_email)

account = Account()
account.balance = 100
account.email = "user@example.com"

print(f"Balance: {account.balance}")
print(f"Email: {account.email}")

try:
    account.balance = -50
except ValueError as e:
    print(f"Error: {e}")

# Lazy property
print("\nLazy property:")

class LazyProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, type=None):
        if obj is None:
            return self

        # Compute once and replace descriptor with value
        value = self.func(obj)
        setattr(obj, self.name, value)
        return value

class Resource:
    def __init__(self, filename):
        self.filename = filename

    @LazyProperty
    def content(self):
        print(f"  Loading {self.filename}...")
        return f"Content of {self.filename}"

resource = Resource("data.txt")
print("Resource created")

print("\nFirst access:")
print(resource.content)

print("\nSecond access:")
print(resource.content)

# Practical example
print("\nPractical example:")

# Combining property with validation
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive")
        self._height = value

    @property
    def area(self):
        """Computed property"""
        return self._width * self._height

    @property
    def perimeter(self):
        """Computed property"""
        return 2 * (self._width + self._height)

rect = Rectangle(10, 5)
print(f"Rectangle: {rect.width}x{rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")

rect.width = 20
print(f"Updated rectangle: {rect.width}x{rect.height}")
print(f"New area: {rect.area}")

  1. celsius = PropertyDescriptor(get_celsius, set_celsius)

    1"""Property as a descriptor"""23# Property implementation4print("Property implementation:")56# This is roughly how @property works7class PropertyDescriptor:8    def __init__(self, fget=None, fset=None, fdel=None):9        self.fget = fget10        self.fset = fset11        self.fdel = fdel12    13    def __get__(self, obj, type=None):14        if obj is None:15            return self16        if self.fget is None:17            raise AttributeError("unreadable attribute")18        return self.fget(obj)19    20    def __set__(self, obj, value):21        if self.fset is None:22            raise AttributeError("can't set attribute")23        self.fset(obj, value)24    25    def __delete__(self, obj):26        if self.fdel is None:27            raise AttributeError("can't delete attribute")28        self.fdel(obj)2930class Temperature:31    def __init__(self, celsius):32        self._celsius = celsius33    34    def get_celsius(self):35        return self._celsius36    37    def set_celsius(self, value):38        self._celsius = value39    40    celsius = PropertyDescriptor(get_celsius(empty), set_celsius(empty))
    outputProperty implementation:
  2. self.fget ← ⟨function Temperature.get_celsius A⟩, self.fset ← ⟨function Temperature.set_celsius B⟩

    7class PropertyDescriptor:8    def __init__(self⟨PropertyDescriptor C⟩, fget⟨function Temperature.get_celsius A⟩=NoneNone, fset⟨function Temperature.set_celsius B⟩=None, fdelNone=None):9        self.fget→ ⟨function Temperature.get_celsius A⟩ = fget⟨function Temperature.get_celsius A⟩10        self.fset→ ⟨function Temperature.set_celsius B⟩ = fset⟨function Temperature.set_celsius B⟩11        self.fdel→ None = fdelNone
  3. celsius ← (empty)

    40    celsius→ (empty) = PropertyDescriptor(get_celsius(empty), set_celsius(empty))4142temp = Temperature(25)43print(f"Celsius: {temp.celsius}")
  4. self._celsius ← 25

    30class Temperature:31    def __init__(self⟨Temperature D⟩, celsius25):32        self._celsius→ 25 = celsius25
  5. temp ← ⟨Temperature D⟩

    42temp→ ⟨Temperature D⟩ = Temperature(25)43print(f"Celsius: {temp.celsius25}")44temp.celsius = 30
  6. def __get__(self, obj, type=None):

    pass 1 of 2
    13def __get__(self⟨PropertyDescriptor C⟩, obj⟨Temperature D⟩, type<class '__main__.Temperature'>=NoneNone):14    if obj is None:15        return self16    if self.fget is None:17        raise AttributeError("unreadable attribute")18    return self.fget(obj⟨Temperature D⟩)
  7. def get_celsius(self):

    pass 1 of 2
    34def get_celsius(self⟨Temperature D⟩):35    return self._celsius25
  8. print(f"Celsius: {temp.celsius}")

    42temp = Temperature(25)43print(f"Celsius: {temp.celsius25}")44temp.celsius = 3045print(f"Updated: {temp.celsius}")
    outputCelsius: 25
  9. def __set__(self, obj, value):

    20def __set__(self⟨PropertyDescriptor C⟩, obj⟨Temperature D⟩, value30):21    if self.fset is None:22        raise AttributeError("can't set attribute")23    self.fset(obj⟨Temperature D⟩, value30)
  10. self._celsius ← 30

    22            raise AttributeError("can't set attribute")23        self.fset(obj⟨Temperature D⟩, value30)24    25    def __delete__(self, obj):26        if self.fdel is None:27            raise AttributeError("can't delete attribute")28        self.fdel(obj)2930class Temperature:31    def __init__(self, celsius):32        self._celsius = celsius33    34    def get_celsius(self):35        return self._celsius36    37    def set_celsius(self⟨Temperature D⟩, value30):38        self._celsius→ 30 = value30
  11. temp.celsius ← 30

    43print(f"Celsius: {temp.celsius}")44temp.celsius→ 30 = 3045print(f"Updated: {temp.celsius30}")
  12. def __get__(self, obj, type=None):

    pass 2 of 2
    13def __get__(self⟨PropertyDescriptor C⟩, obj⟨Temperature D⟩, type<class '__main__.Temperature'>=NoneNone):14    if obj is None:15        return self16    if self.fget is None:17        raise AttributeError("unreadable attribute")18    return self.fget(obj⟨Temperature D⟩)
  13. def get_celsius(self):

    pass 2 of 2
    34def get_celsius(self⟨Temperature D⟩):35    return self._celsius30
  14. print(f"Updated: {temp.celsius}")

    44temp.celsius = 3045print(f"Updated: {temp.celsius30}")4647# Standard @property48print("\nStandard @property:")4950class Circle:51    def __init__(self, radius):52        self._radius = radius53    54    @property55    def radius(self):56        """Get radius"""57        return self._radius58    59    @radius.setter60    def radius(self, value):61        """Set radius"""62        if value < 0:63            raise ValueError("Radius cannot be negative")64        self._radius = value65    66    @property67    def area(self):68        """Computed area"""69        import math70        return math.pi * self._radius ** 27172circle = Circle(5)73print(f"Radius: {circle.radius}")
    outputUpdated: 30
    
    Standard @property:
  15. self._radius ← 5

    50class Circle:51    def __init__(self⟨Circle E⟩, radius5):52        self._radius→ 5 = radius5
  16. circle ← ⟨Circle E⟩

    72circle→ ⟨Circle E⟩ = Circle(5)73print(f"Radius: {circle.radius5}")74print(f"Area: {circle.area:.2f}")
  17. def radius(self):

    pass 1 of 2
    54@property55def radius(self⟨Circle E⟩):56    """Get radius"""57    return self._radius5
  18. print(f"Radius: {circle.radius}")

    72circle = Circle(5)73print(f"Radius: {circle.radius5}")74print(f"Area: {circle.area78.53981633974483:.2f}")
    outputRadius: 5
  19. def area(self):

    pass 1 of 2
    66@property67def area(self⟨Circle E⟩):68    """Computed area"""69    import math70    return math.pi3.141592653589793 * self._radius5 ** 2
  20. print(f"Area: {circle.area:.2f}")

    73print(f"Radius: {circle.radius}")74print(f"Area: {circle.area78.53981633974483:.2f}")7576circle.radius = 1077print(f"New radius: {circle.radius}")
    outputArea: 78.54
  21. self._radius ← 10

    59@radius.setter60def radius(self⟨Circle E⟩, value10):61    """Set radius"""62    if value < 0:63        raise ValueError("Radius cannot be negative")64    self._radius→ 10 = value10
  22. circle.radius ← 10

    76circle.radius→ 10 = 1077print(f"New radius: {circle.radius10}")78print(f"New area: {circle.area:.2f}")
  23. def radius(self):

    pass 2 of 2
    54@property55def radius(self⟨Circle E⟩):56    """Get radius"""57    return self._radius10
  24. print(f"New radius: {circle.radius}")

    76circle.radius = 1077print(f"New radius: {circle.radius10}")78print(f"New area: {circle.area314.1592653589793:.2f}")
    outputNew radius: 10
  25. def area(self):

    pass 2 of 2
    66@property67def area(self⟨Circle E⟩):68    """Computed area"""69    import math70    return math.pi3.141592653589793 * self._radius10 ** 2
  26. print(f"New area: {circle.area:.2f}")

    77print(f"New radius: {circle.radius}")78print(f"New area: {circle.area314.1592653589793:.2f}")7980# Read-only property81print("\nRead-only property:")8283class Person:84    def __init__(self, first_name, last_name):85        self._first_name = first_name86        self._last_name = last_name87    88    @property89    def full_name(self):90        """Read-only computed property"""91        return f"{self._first_name} {self._last_name}"9293person = Person("Alice", "Smith")94print(f"Full name: {person.full_name}")
    outputNew area: 314.16
    
    Read-only property:
  27. self._first_name ← Alice, self._last_name ← Smith

    83class Person:84    def __init__(self⟨Person F⟩, first_nameAlice, last_nameSmith):85        self._first_name→ Alice = first_nameAlice86        self._last_name→ Smith = last_nameSmith
  28. person ← ⟨Person F⟩

    93person→ ⟨Person F⟩ = Person("Alice", "Smith")94print(f"Full name: {person.full_nameAlice Smith}")
  29. def full_name(self):

    88@property89def full_name(self⟨Person F⟩):90    """Read-only computed property"""91    return f"{self._first_nameAlice} {self._last_nameSmith}"
  30. print(f"Full name: {person.full_name}")

    93person = Person("Alice", "Smith")94print(f"Full name: {person.full_nameAlice Smith}")
    outputFull name: Alice Smith
  31. except AttributeError as e:

    97    person.full_name = "Bob Jones"98except AttributeError as e:99    print(f"Error: {eproperty 'full_name' of 'Person' object has no setter}")
    outputError: property 'full_name' of 'Person' object has no setter
  32. print(" Cached property:")

    101# Cached property102print("\nCached property:")
    output
    Cached property:
  33. self.func ← ⟨function DataProcessor.expensive_result G⟩, self.name ← expensive_result

    104class CachedProperty:105    def __init__(self⟨CachedProperty H⟩, func⟨function DataProcessor.expensive_result G⟩):106        self.func→ ⟨function DataProcessor.expensive_result G⟩ = func⟨function DataProcessor.expensive_result G⟩107        self.name→ expensive_result = func.__name__expensive_result
  34. processor = DataProcessor([1, 2, 3, 4, 5])

    131processor = DataProcessor([1, 2, 3, 4, 5])
  35. self.data ← [1, 2, 3, 4, 5]

    122class DataProcessor:123    def __init__(self⟨DataProcessor I⟩, data[1, 2, 3, 4, 5]):124        self.data→ [1, 2, 3, 4, 5] = data[1, 2, 3, 4, 5]
  36. processor ← ⟨DataProcessor I⟩

    131processor→ ⟨DataProcessor I⟩ = DataProcessor([1, 2, 3, 4, 5])132133print("First access:")134result1 = processor.expensive_result55135print(f"Result: {result1}")
    outputFirst access:
  37. cache_name ← _cached_expensive_result

    pass 1 of 2
    109def __get__(self⟨CachedProperty H⟩, obj⟨DataProcessor I⟩, type<class '__main__.DataProcessor'>=NoneNone):110    if obj is None:111        return self112    113    # Check if cached114    cache_name→ _cached_expensive_result = f'_cached_{self.nameexpensive_result}'115    if cache_name not in obj.__dict__:116        # Compute and cache117        print(f"  Computing {self.name}...")118        obj.__dict__[cache_name] = self.func(obj)119    120    return obj.__dict__[cache_name]55
  38. result1 ← 55

    133print("First access:")134result1→ 55 = processor.expensive_result55135print(f"Result: {result155}")136137print("\nSecond access (cached):")138result2 = processor.expensive_result55139print(f"Result: {result2}")
    outputResult: 55
    
    Second access (cached):
  39. cache_name ← _cached_expensive_result

    pass 2 of 2
    109def __get__(self⟨CachedProperty H⟩, obj⟨DataProcessor I⟩, type<class '__main__.DataProcessor'>=NoneNone):110    if obj is None:111        return self112    113    # Check if cached114    cache_name→ _cached_expensive_result = f'_cached_{self.nameexpensive_result}'115    if cache_name not in obj.__dict__:116        # Compute and cache117        print(f"  Computing {self.name}...")118        obj.__dict__[cache_name] = self.func(obj)119    120    return obj.__dict__[cache_name]55
  40. result2 ← 55

    137print("\nSecond access (cached):")138result2→ 55 = processor.expensive_result55139print(f"Result: {result255}")140141# Validated property142print("\nValidated property:")143144class ValidatedProperty:145    def __init__(self, name, validator):146        self.name = name147        self.validator = validator148    149    def __get__(self, obj, type=None):150        if obj is None:151            return self152        return obj.__dict__.get(self.name)153    154    def __set__(self, obj, value):155        if not self.validator(value):156            raise ValueError(f"Invalid value for {self.name}: {value}")157        obj.__dict__[self.name] = value158159def is_positive(value):160    return value > 0161162def is_valid_email(value):163    return "@" in value and "." in value164165class Account:166    balance = ValidatedProperty('balance', is_positive⟨function is_positive J⟩)167    email = ValidatedProperty('email', is_valid_email)
    outputResult: 55
    
    Validated property:
  41. self.name ← balance, self.validator ← ⟨function is_positive J⟩

    pass 1 of 2
    144class ValidatedProperty:145    def __init__(self⟨ValidatedProperty K⟩, namebalance, validator⟨function is_positive J⟩):146        self.name→ balance = namebalance147        self.validator→ ⟨function is_positive J⟩ = validator⟨function is_positive J⟩
  42. balance ← (empty)

    165class Account:166    balance→ (empty) = ValidatedProperty('balance', is_positive⟨function is_positive J⟩)167    email = ValidatedProperty('email', is_valid_email⟨function is_valid_email L⟩)
  43. self.name ← email, self.validator ← ⟨function is_valid_email L⟩

    pass 2 of 2
    144class ValidatedProperty:145    def __init__(self⟨ValidatedProperty M⟩, nameemail, validator⟨function is_valid_email L⟩):146        self.name→ email = nameemail147        self.validator→ ⟨function is_valid_email L⟩ = validator⟨function is_valid_email L⟩
  44. email ← (empty), account ← ⟨Account N⟩

    166    balance = ValidatedProperty('balance', is_positive)167    email→ (empty) = ValidatedProperty('email', is_valid_email⟨function is_valid_email L⟩)168169account→ ⟨Account N⟩ = Account()170account.balance = 100171account.email = "user@example.com"
  45. def __set__(self, obj, value):

    pass 1 of 3
    154def __set__(self⟨ValidatedProperty K⟩, obj⟨Account N⟩, value100):155    if not self.validator(value):156        raise ValueError(f"Invalid value for {self.name}: {value}")
    All 3 passes — pass 1 is the card above
    passselfvalueself.namee
    1⟨ValidatedProperty K⟩100
    2⟨ValidatedProperty M⟩user@example.com
    3⟨ValidatedProperty K⟩-50balanceInvalid value for balance: -50
  46. def is_positive(value):

    pass 1 of 2
    159def is_positive(value100):160    return value100 > 0
  47. obj.__dict__[self.name] ← 100, account.balance ← 100

    156            raise ValueError(f"Invalid value for {self.name}: {value}")157        obj.__dict__[self.name]→ 100 = value100158159def is_positive(value):160    return value > 0161162def is_valid_email(value):163    return "@" in value and "." in value164165class Account:166    balance = ValidatedProperty('balance', is_positive)167    email = ValidatedProperty('email', is_valid_email)168169account = Account()170account.balance→ 100 = 100171account.email = "user@example.com"
  48. def is_valid_email(value):

    162def is_valid_email(valueuser@example.com):163    return "@" in valueuser@example.com and "." in value
  49. obj.__dict__[self.name] ← user@example.com, account.email ← user@example.com

    156            raise ValueError(f"Invalid value for {self.name}: {value}")157        obj.__dict__[self.name]→ user@example.com = valueuser@example.com158159def is_positive(value):160    return value > 0161162def is_valid_email(value):163    return "@" in value and "." in value164165class Account:166    balance = ValidatedProperty('balance', is_positive)167    email = ValidatedProperty('email', is_valid_email)168169account = Account()170account.balance = 100171account.email→ user@example.com = "user@example.com"172173print(f"Balance: {account.balance100}")174print(f"Email: {account.email}")
  50. def __get__(self, obj, type=None):

    pass 1 of 2
    149def __get__(self⟨ValidatedProperty K⟩, obj⟨Account N⟩, type<class '__main__.Account'>=NoneNone):150    if obj is None:151        return self152    return obj.__dict__{'balance': 100, 'email': 'user@example.com'}.get(self.namebalance)
  51. print(f"Balance: {account.balance}")

    173print(f"Balance: {account.balance100}")174print(f"Email: {account.emailuser@example.com}")
    outputBalance: 100
  52. def __get__(self, obj, type=None):

    pass 2 of 2
    149def __get__(self⟨ValidatedProperty M⟩, obj⟨Account N⟩, type<class '__main__.Account'>=NoneNone):150    if obj is None:151        return self152    return obj.__dict__{'balance': 100, 'email': 'user@example.com'}.get(self.nameemail)
  53. print(f"Email: {account.email}")

    173print(f"Balance: {account.balance}")174print(f"Email: {account.emailuser@example.com}")
    outputEmail: user@example.com
  54. def is_positive(value):

    pass 2 of 2
    159def is_positive(value-50):160    return value-50 > 0
  55. if not self.validator(value):

    154def __set__(self, obj, value):155    if not self.validator(value-50):156        raise ValueError(f"Invalid value for {self.namebalance}: {value-50}")157    obj.__dict__[self.name] = value
  56. except ValueError as e:

    177    account.balance = -50178except ValueError as e:179    print(f"Error: {eInvalid value for balance: -50}")
    outputError: Invalid value for balance: -50
  57. print(" Lazy property:")

    181# Lazy property182print("\nLazy property:")
    output
    Lazy property:
  58. self.func ← ⟨function Resource.content O⟩, self.name ← content

    184class LazyProperty:185    def __init__(self⟨LazyProperty P⟩, func⟨function Resource.content O⟩):186        self.func→ ⟨function Resource.content O⟩ = func⟨function Resource.content O⟩187        self.name→ content = func.__name__content
  59. resource = Resource("data.txt")

    207resource = Resource("data.txt")208print("Resource created")
  60. self.filename ← data.txt

    198class Resource:199    def __init__(self⟨Resource Q⟩, filenamedata.txt):200        self.filename→ data.txt = filenamedata.txt
  61. resource ← ⟨Resource Q⟩

    207resource→ ⟨Resource Q⟩ = Resource("data.txt")208print("Resource created")209210print("\nFirst access:")211print(resource.contentContent of data.txt)212213print("\nSecond access:")214print(resource.contentContent of data.txt)215216# Practical example217print("\nPractical example:")218219# Combining property with validation220class Rectangle:221    def __init__(self, width, height):222        self._width = width223        self._height = height224    225    @property226    def width(self):227        return self._width228    229    @width.setter230    def width(self, value):231        if value <= 0:232            raise ValueError("Width must be positive")233        self._width = value234    235    @property236    def height(self):237        return self._height238    239    @height.setter240    def height(self, value):241        if value <= 0:242            raise ValueError("Height must be positive")243        self._height = value244    245    @property246    def area(self):247        """Computed property"""248        return self._width * self._height249    250    @property251    def perimeter(self):252        """Computed property"""253        return 2 * (self._width + self._height)254255rect = Rectangle(10, 5)256print(f"Rectangle: {rect.width}x{rect.height}")
    outputResource created
    
    First access:
    Content of data.txt
    
    Second access:
    Content of data.txt
    
    Practical example:
  62. self._width ← 10, self._height ← 5

    220class Rectangle:221    def __init__(self⟨Rectangle R⟩, width10, height5):222        self._width→ 10 = width10223        self._height→ 5 = height5
  63. rect ← ⟨Rectangle R⟩

    255rect→ ⟨Rectangle R⟩ = Rectangle(10, 5)256print(f"Rectangle: {rect.width10}x{rect.height5}")257print(f"Area: {rect.area}")
  64. def width(self):

    pass 1 of 2
    225@property226def width(self⟨Rectangle R⟩):227    return self._width10
  65. def height(self):

    pass 1 of 2
    235@property236def height(self⟨Rectangle R⟩):237    return self._height5
  66. print(f"Rectangle: {rect.width}x{rect.height}")

    255rect = Rectangle(10, 5)256print(f"Rectangle: {rect.width10}x{rect.height5}")257print(f"Area: {rect.area50}")258print(f"Perimeter: {rect.perimeter}")
    outputRectangle: 10x5
  67. def area(self):

    pass 1 of 2
    245@property246def area(self⟨Rectangle R⟩):247    """Computed property"""248    return self._width10 * self._height5
  68. print(f"Area: {rect.area}")

    256print(f"Rectangle: {rect.width}x{rect.height}")257print(f"Area: {rect.area50}")258print(f"Perimeter: {rect.perimeter30}")
    outputArea: 50
  69. def perimeter(self):

    250@property251def perimeter(self⟨Rectangle R⟩):252    """Computed property"""253    return 2 * (self._width10 + self._height5)
  70. print(f"Perimeter: {rect.perimeter}")

    257print(f"Area: {rect.area}")258print(f"Perimeter: {rect.perimeter30}")259260rect.width = 20261print(f"Updated rectangle: {rect.width}x{rect.height}")
    outputPerimeter: 30
  71. self._width ← 20

    229@width.setter230def width(self⟨Rectangle R⟩, value20):231    if value <= 0:232        raise ValueError("Width must be positive")233    self._width→ 20 = value20
  72. rect.width ← 20

    260rect.width→ 20 = 20261print(f"Updated rectangle: {rect.width20}x{rect.height5}")262print(f"New area: {rect.area}")
  73. def width(self):

    pass 2 of 2
    225@property226def width(self⟨Rectangle R⟩):227    return self._width20
  74. def height(self):

    pass 2 of 2
    235@property236def height(self⟨Rectangle R⟩):237    return self._height5
  75. print(f"Updated rectangle: {rect.width}x{rect.height}")

    260rect.width = 20261print(f"Updated rectangle: {rect.width20}x{rect.height5}")262print(f"New area: {rect.area100}")
    outputUpdated rectangle: 20x5
  76. def area(self):

    pass 2 of 2
    245@property246def area(self⟨Rectangle R⟩):247    """Computed property"""248    return self._width20 * self._height5
  77. print(f"New area: {rect.area}")

    261print(f"Updated rectangle: {rect.width}x{rect.height}")262print(f"New area: {rect.area100}")
    outputNew area: 100

Properties are the most common descriptor. Understanding that they are descriptors helps you build more advanced patterns like cached or lazy properties.

property descriptor Understanding that `@property` is syntactic sugar for a descriptor that calls your getter/setter/deleter functions.

Custom Descriptors

initial_score
descriptor_custom.py
Replay: real traced execution (multi-file project)
"""Custom descriptor classes"""

# Typed descriptor
print("Typed descriptor:")

class TypedDescriptor:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")
        obj.__dict__[self.name] = value

class Person:
    name = TypedDescriptor('name', str)
    age = TypedDescriptor('age', int)
    height = TypedDescriptor('height', float)

person = Person()
person.name = "Alice"
person.age = 30
person.height = 1.65

print(f"Person: {person.name}, {person.age}, {person.height}m")

try:
    person.age = "thirty"
except TypeError as e:
    print(f"Error: {e}")

# Bounded descriptor
print("\nBounded descriptor:")

class BoundedNumber:
    def __init__(self, name, min_value=None, max_value=None):
        self.name = name
        self.min_value = min_value
        self.max_value = max_value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} must be >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} must be <= {self.max_value}")
        obj.__dict__[self.name] = value

class Score:
    value = BoundedNumber('value', min_value=0, max_value=100)

score = Score()
initial_score = 85
score.value = initial_score
print(f"Score: {score.value}")

try:
    score.value = 150
except ValueError as e:
    print(f"Error: {e}")

# String descriptor
print("\nString descriptor:")

class ValidatedString:
    def __init__(self, name, min_length=0, max_length=None, pattern=None):
        self.name = name
        self.min_length = min_length
        self.max_length = max_length
        self.pattern = pattern

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} must be a string")

        if len(value) < self.min_length:
            raise ValueError(f"{self.name} must be at least {self.min_length} characters")

        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.name} must be at most {self.max_length} characters")

        if self.pattern:
            import re
            if not re.match(self.pattern, value):
                raise ValueError(f"{self.name} doesn't match pattern")

        obj.__dict__[self.name] = value

class User:
    username = ValidatedString('username', min_length=3, max_length=20)
    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')

user = User()
user.username = "alice"
user.email = "alice@example.com"

print(f"User: {user.username}, {user.email}")

try:
    user.username = "ab"  # Too short
except ValueError as e:
    print(f"Error: {e}")

# Choice descriptor
print("\nChoice descriptor:")

class Choice:
    def __init__(self, name, choices):
        self.name = name
        self.choices = choices

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if value not in self.choices:
            raise ValueError(f"{self.name} must be one of {self.choices}")
        obj.__dict__[self.name] = value

class Status:
    value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])

status = Status()
status.value = 'active'
print(f"Status: {status.value}")

try:
    status.value = 'unknown'
except ValueError as e:
    print(f"Error: {e}")

# Logged descriptor
print("\nLogged descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.name)
        print(f"  Getting {self.name}: {value}")
        return value

    def __set__(self, obj, value):
        print(f"  Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")
        obj.__dict__[self.name] = value

class TrackedObject:
    x = LoggedDescriptor('x')
    y = LoggedDescriptor('y')

tracked = TrackedObject()
tracked.x = 10
tracked.y = 20
print(f"Sum: {tracked.x + tracked.y}")

# Auto-converting descriptor
print("\nAuto-converting descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        # Auto-convert to int
        try:
            obj.__dict__[self.name] = int(value)
        except (TypeError, ValueError) as e:
            raise ValueError(f"Cannot convert {value} to int")

class Config:
    port = IntDescriptor('port')
    timeout = IntDescriptor('timeout')

config = Config()
config.port = "8080"  # String converted to int
config.timeout = 30.5  # Float converted to int

print(f"Port: {config.port} (type: {type(config.port).__name__})")
print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")

# Practical example
print("\nPractical example:")

# Database field descriptor
class Field:
    def __init__(self, name, field_type, required=False, default=None):
        self.name = name
        self.field_type = field_type
        self.required = required
        self.default = default

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, self.default)

    def __set__(self, obj, value):
        if value is None:
            if self.required:
                raise ValueError(f"{self.name} is required")
            obj.__dict__[self.name] = self.default
        elif not isinstance(value, self.field_type):
            raise TypeError(f"{self.name} must be {self.field_type.__name__}")
        else:
            obj.__dict__[self.name] = value

class DatabaseRecord:
    id = Field('id', int, required=True)
    name = Field('name', str, required=True)
    email = Field('email', str)
    age = Field('age', int, default=0)

record = DatabaseRecord()
record.id = 1
record.name = "Alice"
record.email = "alice@example.com"

print(f"Record: id={record.id}, name={record.name}, email={record.email}, age={record.age}")

try:
    record2 = DatabaseRecord()
    record2.name = "Bob"
    # Missing required 'id'
except ValueError as e:
    print(f"Error: {e}")

"""Custom descriptor classes"""

# Typed descriptor
print("Typed descriptor:")

class TypedDescriptor:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")
        obj.__dict__[self.name] = value

class Person:
    name = TypedDescriptor('name', str)
    age = TypedDescriptor('age', int)
    height = TypedDescriptor('height', float)

person = Person()
person.name = "Alice"
person.age = 30
person.height = 1.65

print(f"Person: {person.name}, {person.age}, {person.height}m")

try:
    person.age = "thirty"
except TypeError as e:
    print(f"Error: {e}")

# Bounded descriptor
print("\nBounded descriptor:")

class BoundedNumber:
    def __init__(self, name, min_value=None, max_value=None):
        self.name = name
        self.min_value = min_value
        self.max_value = max_value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} must be >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} must be <= {self.max_value}")
        obj.__dict__[self.name] = value

class Score:
    value = BoundedNumber('value', min_value=0, max_value=100)

score = Score()
initial_score = 60
score.value = initial_score
print(f"Score: {score.value}")

try:
    score.value = 150
except ValueError as e:
    print(f"Error: {e}")

# String descriptor
print("\nString descriptor:")

class ValidatedString:
    def __init__(self, name, min_length=0, max_length=None, pattern=None):
        self.name = name
        self.min_length = min_length
        self.max_length = max_length
        self.pattern = pattern

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} must be a string")

        if len(value) < self.min_length:
            raise ValueError(f"{self.name} must be at least {self.min_length} characters")

        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.name} must be at most {self.max_length} characters")

        if self.pattern:
            import re
            if not re.match(self.pattern, value):
                raise ValueError(f"{self.name} doesn't match pattern")

        obj.__dict__[self.name] = value

class User:
    username = ValidatedString('username', min_length=3, max_length=20)
    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')

user = User()
user.username = "alice"
user.email = "alice@example.com"

print(f"User: {user.username}, {user.email}")

try:
    user.username = "ab"  # Too short
except ValueError as e:
    print(f"Error: {e}")

# Choice descriptor
print("\nChoice descriptor:")

class Choice:
    def __init__(self, name, choices):
        self.name = name
        self.choices = choices

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if value not in self.choices:
            raise ValueError(f"{self.name} must be one of {self.choices}")
        obj.__dict__[self.name] = value

class Status:
    value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])

status = Status()
status.value = 'active'
print(f"Status: {status.value}")

try:
    status.value = 'unknown'
except ValueError as e:
    print(f"Error: {e}")

# Logged descriptor
print("\nLogged descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.name)
        print(f"  Getting {self.name}: {value}")
        return value

    def __set__(self, obj, value):
        print(f"  Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")
        obj.__dict__[self.name] = value

class TrackedObject:
    x = LoggedDescriptor('x')
    y = LoggedDescriptor('y')

tracked = TrackedObject()
tracked.x = 10
tracked.y = 20
print(f"Sum: {tracked.x + tracked.y}")

# Auto-converting descriptor
print("\nAuto-converting descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        # Auto-convert to int
        try:
            obj.__dict__[self.name] = int(value)
        except (TypeError, ValueError) as e:
            raise ValueError(f"Cannot convert {value} to int")

class Config:
    port = IntDescriptor('port')
    timeout = IntDescriptor('timeout')

config = Config()
config.port = "8080"  # String converted to int
config.timeout = 30.5  # Float converted to int

print(f"Port: {config.port} (type: {type(config.port).__name__})")
print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")

# Practical example
print("\nPractical example:")

# Database field descriptor
class Field:
    def __init__(self, name, field_type, required=False, default=None):
        self.name = name
        self.field_type = field_type
        self.required = required
        self.default = default

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, self.default)

    def __set__(self, obj, value):
        if value is None:
            if self.required:
                raise ValueError(f"{self.name} is required")
            obj.__dict__[self.name] = self.default
        elif not isinstance(value, self.field_type):
            raise TypeError(f"{self.name} must be {self.field_type.__name__}")
        else:
            obj.__dict__[self.name] = value

class DatabaseRecord:
    id = Field('id', int, required=True)
    name = Field('name', str, required=True)
    email = Field('email', str)
    age = Field('age', int, default=0)

record = DatabaseRecord()
record.id = 1
record.name = "Alice"
record.email = "alice@example.com"

print(f"Record: id={record.id}, name={record.name}, email={record.email}, age={record.age}")

try:
    record2 = DatabaseRecord()
    record2.name = "Bob"
    # Missing required 'id'
except ValueError as e:
    print(f"Error: {e}")

"""Custom descriptor classes"""

# Typed descriptor
print("Typed descriptor:")

class TypedDescriptor:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")
        obj.__dict__[self.name] = value

class Person:
    name = TypedDescriptor('name', str)
    age = TypedDescriptor('age', int)
    height = TypedDescriptor('height', float)

person = Person()
person.name = "Alice"
person.age = 30
person.height = 1.65

print(f"Person: {person.name}, {person.age}, {person.height}m")

try:
    person.age = "thirty"
except TypeError as e:
    print(f"Error: {e}")

# Bounded descriptor
print("\nBounded descriptor:")

class BoundedNumber:
    def __init__(self, name, min_value=None, max_value=None):
        self.name = name
        self.min_value = min_value
        self.max_value = max_value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} must be >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} must be <= {self.max_value}")
        obj.__dict__[self.name] = value

class Score:
    value = BoundedNumber('value', min_value=0, max_value=100)

score = Score()
initial_score = 100
score.value = initial_score
print(f"Score: {score.value}")

try:
    score.value = 150
except ValueError as e:
    print(f"Error: {e}")

# String descriptor
print("\nString descriptor:")

class ValidatedString:
    def __init__(self, name, min_length=0, max_length=None, pattern=None):
        self.name = name
        self.min_length = min_length
        self.max_length = max_length
        self.pattern = pattern

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} must be a string")

        if len(value) < self.min_length:
            raise ValueError(f"{self.name} must be at least {self.min_length} characters")

        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.name} must be at most {self.max_length} characters")

        if self.pattern:
            import re
            if not re.match(self.pattern, value):
                raise ValueError(f"{self.name} doesn't match pattern")

        obj.__dict__[self.name] = value

class User:
    username = ValidatedString('username', min_length=3, max_length=20)
    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')

user = User()
user.username = "alice"
user.email = "alice@example.com"

print(f"User: {user.username}, {user.email}")

try:
    user.username = "ab"  # Too short
except ValueError as e:
    print(f"Error: {e}")

# Choice descriptor
print("\nChoice descriptor:")

class Choice:
    def __init__(self, name, choices):
        self.name = name
        self.choices = choices

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if value not in self.choices:
            raise ValueError(f"{self.name} must be one of {self.choices}")
        obj.__dict__[self.name] = value

class Status:
    value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])

status = Status()
status.value = 'active'
print(f"Status: {status.value}")

try:
    status.value = 'unknown'
except ValueError as e:
    print(f"Error: {e}")

# Logged descriptor
print("\nLogged descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.name)
        print(f"  Getting {self.name}: {value}")
        return value

    def __set__(self, obj, value):
        print(f"  Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")
        obj.__dict__[self.name] = value

class TrackedObject:
    x = LoggedDescriptor('x')
    y = LoggedDescriptor('y')

tracked = TrackedObject()
tracked.x = 10
tracked.y = 20
print(f"Sum: {tracked.x + tracked.y}")

# Auto-converting descriptor
print("\nAuto-converting descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        # Auto-convert to int
        try:
            obj.__dict__[self.name] = int(value)
        except (TypeError, ValueError) as e:
            raise ValueError(f"Cannot convert {value} to int")

class Config:
    port = IntDescriptor('port')
    timeout = IntDescriptor('timeout')

config = Config()
config.port = "8080"  # String converted to int
config.timeout = 30.5  # Float converted to int

print(f"Port: {config.port} (type: {type(config.port).__name__})")
print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")

# Practical example
print("\nPractical example:")

# Database field descriptor
class Field:
    def __init__(self, name, field_type, required=False, default=None):
        self.name = name
        self.field_type = field_type
        self.required = required
        self.default = default

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, self.default)

    def __set__(self, obj, value):
        if value is None:
            if self.required:
                raise ValueError(f"{self.name} is required")
            obj.__dict__[self.name] = self.default
        elif not isinstance(value, self.field_type):
            raise TypeError(f"{self.name} must be {self.field_type.__name__}")
        else:
            obj.__dict__[self.name] = value

class DatabaseRecord:
    id = Field('id', int, required=True)
    name = Field('name', str, required=True)
    email = Field('email', str)
    age = Field('age', int, default=0)

record = DatabaseRecord()
record.id = 1
record.name = "Alice"
record.email = "alice@example.com"

print(f"Record: id={record.id}, name={record.name}, email={record.email}, age={record.age}")

try:
    record2 = DatabaseRecord()
    record2.name = "Bob"
    # Missing required 'id'
except ValueError as e:
    print(f"Error: {e}")

  1. """Custom descriptor classes"""

    1"""Custom descriptor classes"""23# Typed descriptor4print("Typed descriptor:")56class TypedDescriptor:7    def __init__(self, name, expected_type):8        self.name = name9        self.expected_type = expected_type10    11    def __get__(self, obj, type=None):12        if obj is None:13            return self14        return obj.__dict__.get(self.name)15    16    def __set__(self, obj, value):17        if not isinstance(value, self.expected_type):18            raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19        obj.__dict__[self.name] = value2021class Person:22    name = TypedDescriptor('name', str)23    age = TypedDescriptor('age', int)
    outputTyped descriptor:
  2. self.name ← name, self.expected_type ← <class 'str'>

    pass 1 of 3
    6class TypedDescriptor:7    def __init__(self⟨TypedDescriptor A⟩, namename, expected_type<class 'str'>):8        self.name→ name = namename9        self.expected_type→ <class 'str'> = expected_type<class 'str'>
    All 3 passes — pass 1 is the card above
    passselfnameexpected_typeself.nameself.expected_type
    1⟨TypedDescriptor A⟩name<class 'str'>name<class 'str'>
    2⟨TypedDescriptor B⟩age<class 'int'>age<class 'int'>
    3⟨TypedDescriptor C⟩height<class 'float'>height<class 'float'>
  3. name ← (empty)

    21class Person:22    name→ (empty) = TypedDescriptor('name', str)23    age = TypedDescriptor('age', int)24    height = TypedDescriptor('height', float)
  4. age ← (empty)

    22name = TypedDescriptor('name', str)23age→ (empty) = TypedDescriptor('age', int)24height = TypedDescriptor('height', float)
  5. height ← (empty), person ← ⟨Person D⟩

    23    age = TypedDescriptor('age', int)24    height→ (empty) = TypedDescriptor('height', float)2526person→ ⟨Person D⟩ = Person()27person.name = "Alice"28person.age = 30
  6. obj.__dict__[self.name] ← Alice

    pass 1 of 4
    16def __set__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, valueAlice):17    if not isinstance(value, self.expected_type):18        raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19    obj.__dict__[self.name]→ Alice = valueAlice
    All 4 passes — pass 1 is the card above
    passselfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]
    1⟨TypedDescriptor A⟩AliceAlice
    2⟨TypedDescriptor B⟩3030
    3⟨TypedDescriptor C⟩1.651.65
    4⟨TypedDescriptor B⟩thirty<class 'int'>ageintage must be int, got str
  7. person.name ← Alice

    26person = Person()27person.name→ Alice = "Alice"28person.age = 3029person.height = 1.65
  8. person.age ← 30

    27person.name = "Alice"28person.age→ 30 = 3029person.height = 1.65
  9. person.height ← 1.65

    28person.age = 3029person.height→ 1.65 = 1.653031print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")
  10. def __get__(self, obj, type=None):

    pass 1 of 3
    11def __get__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, type<class '__main__.Person'>=NoneNone):12    if obj is None:13        return self14    return obj.__dict__{'name': 'Alice', 'age': 30, 'height': 1.65}.get(self.namename)
    All 3 passes — pass 1 is the card above
    passselfself.name
    1⟨TypedDescriptor A⟩name
    2⟨TypedDescriptor B⟩age
    3⟨TypedDescriptor C⟩height
  11. print(f"Person: {person.name}, {person.age}, {person.height}m")

    31print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")
    outputPerson: Alice, 30, 1.65m
  12. if not isinstance(value, self.expected_type):

    16def __set__(self, obj, value):17    if not isinstance(valuethirty, self.expected_type<class 'int'>):18        raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}, got {type(valuethirty).__name__}")19    obj.__dict__[self.name] = value
  13. except TypeError as e:

    34    person.age = "thirty"35except TypeError as e:36    print(f"Error: {eage must be int, got str}")
    outputError: age must be int, got str
  14. print(" Bounded descriptor:")

    38# Bounded descriptor39print("\nBounded descriptor:")4041class BoundedNumber:42    def __init__(self, name, min_value=None, max_value=None):43        self.name = name44        self.min_value = min_value45        self.max_value = max_value46    47    def __get__(self, obj, type=None):48        if obj is None:49            return self50        return obj.__dict__.get(self.name)51    52    def __set__(self, obj, value):53        if self.min_value is not None and value < self.min_value:54            raise ValueError(f"{self.name} must be >= {self.min_value}")55        if self.max_value is not None and value > self.max_value:56            raise ValueError(f"{self.name} must be <= {self.max_value}")57        obj.__dict__[self.name] = value5859class Score:60    value = BoundedNumber('value', min_value=0, max_value=100)
    output
    Bounded descriptor:
  15. self.name ← value, self.min_value ← 0, self.max_value ← 100

    41class BoundedNumber:42    def __init__(self⟨BoundedNumber E⟩, namevalue, min_value0=NoneNone, max_value100=None):43        self.name→ value = namevalue44        self.min_value→ 0 = min_value045        self.max_value→ 100 = max_value100
  16. value ← (empty), score ← ⟨Score F⟩, initial_score ← 85

    59class Score:60    value→ (empty) = BoundedNumber('value', min_value=0, max_value=100)6162score→ ⟨Score F⟩ = Score()63initial_score→ 85 = 85  #@initial_score=60, 10064score.value = initial_score8565print(f"Score: {score.value}")
  17. obj.__dict__[self.name] ← 85

    pass 1 of 2
    52def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value85):53    if self.min_value is not None and value < self.min_value:54        raise ValueError(f"{self.name} must be >= {self.min_value}")55    if self.max_value is not None and value > self.max_value:56        raise ValueError(f"{self.name} must be <= {self.max_value}")57    obj.__dict__[self.name]→ 85 = value85
  18. score.value ← 85

    63initial_score = 85  #@initial_score=60, 10064score.value→ 85 = initial_score8565print(f"Score: {score.value85}")
  19. def __get__(self, obj, type=None):

    47def __get__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, type<class '__main__.Score'>=NoneNone):48    if obj is None:49        return self50    return obj.__dict__{'value': 85}.get(self.namevalue)
  20. print(f"Score: {score.value}")

    64score.value = initial_score65print(f"Score: {score.value85}")
    outputScore: 85
  21. def __set__(self, obj, value):

    pass 2 of 2
    52def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value150):53    if self.min_value is not None and value < self.min_value:54        raise ValueError(f"{self.name} must be >= {self.min_value}")
  22. if self.max_value is not None and value > self.max_value:

    54    raise ValueError(f"{self.name} must be >= {self.min_value}")55if self.max_value100 is not None and value150 > self.max_value:56    raise ValueError(f"{self.namevalue} must be <= {self.max_value100}")57obj.__dict__[self.name] = value
  23. except ValueError as e:

    68    score.value = 15069except ValueError as e:70    print(f"Error: {evalue must be <= 100}")
    outputError: value must be <= 100
  24. print(" String descriptor:")

    72# String descriptor73print("\nString descriptor:")7475class ValidatedString:76    def __init__(self, name, min_length=0, max_length=None, pattern=None):77        self.name = name78        self.min_length = min_length79        self.max_length = max_length80        self.pattern = pattern81    82    def __get__(self, obj, type=None):83        if obj is None:84            return self85        return obj.__dict__.get(self.name)86    87    def __set__(self, obj, value):88        if not isinstance(value, str):89            raise TypeError(f"{self.name} must be a string")90        91        if len(value) < self.min_length:92            raise ValueError(f"{self.name} must be at least {self.min_length} characters")93        94        if self.max_length and len(value) > self.max_length:95            raise ValueError(f"{self.name} must be at most {self.max_length} characters")96        97        if self.pattern:98            import re99            if not re.match(self.pattern, value):100                raise ValueError(f"{self.name} doesn't match pattern")101        102        obj.__dict__[self.name] = value103104class User:105    username = ValidatedString('username', min_length=3, max_length=20)106    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
    output
    String descriptor:
  25. self.name ← username, self.min_length ← 3, self.max_length ← 20

    pass 1 of 2
    75class ValidatedString:76    def __init__(self⟨ValidatedString G⟩, nameusername, min_length3=0, max_length20=NoneNone, patternNone=None):77        self.name→ username = nameusername78        self.min_length→ 3 = min_length379        self.max_length→ 20 = max_length2080        self.pattern→ None = patternNone
  26. username ← (empty)

    104class User:105    username→ (empty) = ValidatedString('username', min_length=3, max_length=20)106    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
  27. self.name ← email, self.min_length ← 0, self.max_length ← None

    pass 2 of 2
    75class ValidatedString:76    def __init__(self⟨ValidatedString H⟩, nameemail, min_length0=0, max_lengthNone=NoneNone, pattern^[\w\.-]+@[\w\.-]+\.\w+$=None):77        self.name→ email = nameemail78        self.min_length→ 0 = min_length079        self.max_length→ None = max_lengthNone80        self.pattern→ ^[\w\.-]+@[\w\.-]+\.\w+$ = pattern^[\w\.-]+@[\w\.-]+\.\w+$
  28. email ← (empty), user ← ⟨User I⟩

    105    username = ValidatedString('username', min_length=3, max_length=20)106    email→ (empty) = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')107108user→ ⟨User I⟩ = User()109user.username = "alice"110user.email = "alice@example.com"
  29. obj.__dict__[self.name] ← alice

    pass 1 of 3
    87def __set__(self⟨ValidatedString G⟩, obj⟨User I⟩, valuealice):88    if not isinstance(value, str):89        raise TypeError(f"{self.name} must be a string")90    91    if len(value) < self.min_length:92        raise ValueError(f"{self.name} must be at least {self.min_length} characters")93    94    if self.max_length and len(value) > self.max_length:95        raise ValueError(f"{self.name} must be at most {self.max_length} characters")96    97    if self.pattern:98        import re99        if not re.match(self.pattern, value):100            raise ValueError(f"{self.name} doesn't match pattern")101    102    obj.__dict__[self.name]→ alice = valuealice
    All 3 passes — pass 1 is the card above
    passselfvalueself.patternself.min_lengthself.nameeobj.__dict__[self.name]
    1⟨ValidatedString G⟩alicealice
    2⟨ValidatedString H⟩alice@example.com^[\w\.-]+@[\w\.-]+\.\w+$
    3⟨ValidatedString G⟩ab3usernameusername must be at least 3 characters
  30. user.username ← alice

    108user = User()109user.username→ alice = "alice"110user.email = "alice@example.com"
  31. if self.pattern:

    97if self.pattern^[\w\.-]+@[\w\.-]+\.\w+$:98    import re99    if not re.match(self.pattern, value):
  32. obj.__dict__[self.name] ← alice@example.com

    102obj.__dict__[self.name]→ alice@example.com = valuealice@example.com
  33. user.email ← alice@example.com

    109user.username = "alice"110user.email→ alice@example.com = "alice@example.com"111112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")
  34. def __get__(self, obj, type=None):

    pass 1 of 2
    82def __get__(self⟨ValidatedString G⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83    if obj is None:84        return self85    return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameusername)
  35. def __get__(self, obj, type=None):

    pass 2 of 2
    82def __get__(self⟨ValidatedString H⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83    if obj is None:84        return self85    return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameemail)
  36. print(f"User: {user.username}, {user.email}")

    112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")
    outputUser: alice, alice@example.com
  37. if len(value) < self.min_length:

    91if len(valueab) < self.min_length3:92    raise ValueError(f"{self.nameusername} must be at least {self.min_length3} characters")
  38. except ValueError as e:

    115    user.username = "ab"  # Too short116except ValueError as e:117    print(f"Error: {eusername must be at least 3 characters}")
    outputError: username must be at least 3 characters
  39. print(" Choice descriptor:")

    119# Choice descriptor120print("\nChoice descriptor:")121122class Choice:123    def __init__(self, name, choices):124        self.name = name125        self.choices = choices126    127    def __get__(self, obj, type=None):128        if obj is None:129            return self130        return obj.__dict__.get(self.name)131    132    def __set__(self, obj, value):133        if value not in self.choices:134            raise ValueError(f"{self.name} must be one of {self.choices}")135        obj.__dict__[self.name] = value136137class Status:138    value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])
    output
    Choice descriptor:
  40. self.name ← value, self.choices ← ['pending', 'active', 'completed', 'cancelled']

    122class Choice:123    def __init__(self⟨Choice J⟩, namevalue, choices['pending', 'active', 'completed', 'cancelled']):124        self.name→ value = namevalue125        self.choices→ ['pending', 'active', 'completed', 'cancelled'] = choices['pending', 'active', 'completed', 'cancelled']
  41. value ← (empty), status ← ⟨Status K⟩

    137class Status:138    value→ (empty) = Choice('value', ['pending', 'active', 'completed', 'cancelled'])139140status→ ⟨Status K⟩ = Status()141status.value = 'active'142print(f"Status: {status.value}")
  42. obj.__dict__[self.name] ← active

    pass 1 of 2
    132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueactive):133    if value not in self.choices:134        raise ValueError(f"{self.name} must be one of {self.choices}")135    obj.__dict__[self.name]→ active = valueactive
  43. status.value ← active

    140status = Status()141status.value→ active = 'active'142print(f"Status: {status.valueactive}")
  44. def __get__(self, obj, type=None):

    127def __get__(self⟨Choice J⟩, obj⟨Status K⟩, type<class '__main__.Status'>=NoneNone):128    if obj is None:129        return self130    return obj.__dict__{'value': 'active'}.get(self.namevalue)
  45. print(f"Status: {status.value}")

    141status.value = 'active'142print(f"Status: {status.valueactive}")
    outputStatus: active
  46. def __set__(self, obj, value):

    pass 2 of 2
    132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueunknown):133    if value not in self.choices:134        raise ValueError(f"{self.name} must be one of {self.choices}")
  47. if value not in self.choices:

    132def __set__(self, obj, value):133    if valueunknown not in self.choices['pending', 'active', 'completed', 'cancelled']:134        raise ValueError(f"{self.namevalue} must be one of {self.choices['pending', 'active', 'completed', 'cancelled']}")135    obj.__dict__[self.name] = value
  48. except ValueError as e:

    145    status.value = 'unknown'146except ValueError as e:147    print(f"Error: {evalue must be one of ['pending', 'active', 'completed', 'cancelled']}")
    outputError: value must be one of ['pending', 'active', 'completed', 'cancelled']
  49. print(" Logged descriptor:")

    149# Logged descriptor150print("\nLogged descriptor:")151152class LoggedDescriptor:153    def __init__(self, name):154        self.name = name155    156    def __get__(self, obj, type=None):157        if obj is None:158            return self159        value = obj.__dict__.get(self.name)160        print(f"  Getting {self.name}: {value}")161        return value162    163    def __set__(self, obj, value):164        print(f"  Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")165        obj.__dict__[self.name] = value166167class TrackedObject:168    x = LoggedDescriptor('x')169    y = LoggedDescriptor('y')
    output
    Logged descriptor:
  50. self.name ← x

    pass 1 of 2
    152class LoggedDescriptor:153    def __init__(self⟨LoggedDescriptor L⟩, namex):154        self.name→ x = namex
  51. x ← (empty)

    167class TrackedObject:168    x→ (empty) = LoggedDescriptor('x')169    y = LoggedDescriptor('y')
  52. self.name ← y

    pass 2 of 2
    152class LoggedDescriptor:153    def __init__(self⟨LoggedDescriptor M⟩, namey):154        self.name→ y = namey
  53. y ← (empty), tracked ← ⟨TrackedObject N⟩

    168    x = LoggedDescriptor('x')169    y→ (empty) = LoggedDescriptor('y')170171tracked→ ⟨TrackedObject N⟩ = TrackedObject()172tracked.x = 10173tracked.y = 20
  54. obj.__dict__[self.name] ← 10

    pass 1 of 2
    163def __set__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, value10):164    print(f"  Setting {self.namex}: {obj.__dict__{}.get(self.name)} -> {value10}")165    obj.__dict__[self.name]→ 10 = value10
    output  Setting x: None -> 10
  55. tracked.x ← 10

    171tracked = TrackedObject()172tracked.x→ 10 = 10173tracked.y = 20174print(f"Sum: {tracked.x + tracked.y}")
  56. obj.__dict__[self.name] ← 20

    pass 2 of 2
    163def __set__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, value20):164    print(f"  Setting {self.namey}: {obj.__dict__{'x': 10}.get(self.name)} -> {value20}")165    obj.__dict__[self.name]→ 20 = value20
    output  Setting y: None -> 20
  57. tracked.y ← 20

    172tracked.x = 10173tracked.y→ 20 = 20174print(f"Sum: {tracked.x10 + tracked.y20}")
  58. value ← 10

    pass 1 of 2
    156def __get__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157    if obj is None:158        return self159    value→ 10 = obj.__dict__{'x': 10, 'y': 20}.get(self.namex)160    print(f"  Getting {self.namex}: {value10}")161    return value10
    output  Getting x: 10
  59. value ← 20

    pass 2 of 2
    156def __get__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157    if obj is None:158        return self159    value→ 20 = obj.__dict__{'x': 10, 'y': 20}.get(self.namey)160    print(f"  Getting {self.namey}: {value20}")161    return value20
    output  Getting y: 20
  60. print(f"Sum: {tracked.x + tracked.y}")

    173tracked.y = 20174print(f"Sum: {tracked.x10 + tracked.y20}")175176# Auto-converting descriptor177print("\nAuto-converting descriptor:")178179class IntDescriptor:180    def __init__(self, name):181        self.name = name182    183    def __get__(self, obj, type=None):184        if obj is None:185            return self186        return obj.__dict__.get(self.name)187    188    def __set__(self, obj, value):189        # Auto-convert to int190        try:191            obj.__dict__[self.name] = int(value)192        except (TypeError, ValueError) as e:193            raise ValueError(f"Cannot convert {value} to int")194195class Config:196    port = IntDescriptor('port')197    timeout = IntDescriptor('timeout')
    outputSum: 30
    
    Auto-converting descriptor:
  61. self.name ← port

    pass 1 of 2
    179class IntDescriptor:180    def __init__(self⟨IntDescriptor O⟩, nameport):181        self.name→ port = nameport
  62. port ← (empty)

    195class Config:196    port→ (empty) = IntDescriptor('port')197    timeout = IntDescriptor('timeout')
  63. self.name ← timeout

    pass 2 of 2
    179class IntDescriptor:180    def __init__(self⟨IntDescriptor P⟩, nametimeout):181        self.name→ timeout = nametimeout
  64. timeout ← (empty), config ← ⟨Config Q⟩

    196    port = IntDescriptor('port')197    timeout→ (empty) = IntDescriptor('timeout')198199config→ ⟨Config Q⟩ = Config()200config.port = "8080"  # String converted to int201config.timeout = 30.5  # Float converted to int
  65. def __set__(self, obj, value): # Auto-convert to int

    pass 1 of 2
    188def __set__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, value8080):189    # Auto-convert to int190    try:191        obj.__dict__[self.name] = int(value)
  66. obj.__dict__[self.name] ← 8080

    pass 1 of 2
    189# Auto-convert to int190try:191    obj.__dict__[self.name]→ 8080 = int(value8080)192except (TypeError, ValueError) as e:
  67. config.port ← 8080

    199config = Config()200config.port→ 8080 = "8080"  # String converted to int201config.timeout = 30.5  # Float converted to int
  68. def __set__(self, obj, value): # Auto-convert to int

    pass 2 of 2
    188def __set__(self⟨IntDescriptor P⟩, obj⟨Config Q⟩, value30.5):189    # Auto-convert to int190    try:191        obj.__dict__[self.name] = int(value)
  69. obj.__dict__[self.name] ← 30

    pass 2 of 2
    189# Auto-convert to int190try:191    obj.__dict__[self.name]→ 30 = int(value30.5)192except (TypeError, ValueError) as e:
  70. config.timeout ← 30

    200config.port = "8080"  # String converted to int201config.timeout→ 30 = 30.5  # Float converted to int202203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")
  71. def __get__(self, obj, type=None):

    pass 1 of 4
    183def __get__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, type<class '__main__.Config'>=NoneNone):184    if obj is None:185        return self186    return obj.__dict__{'port': 8080, 'timeout': 30}.get(self.nameport)
    All 4 passes — pass 1 is the card above
    passselfself.name
    1⟨IntDescriptor O⟩port
    2⟨IntDescriptor O⟩port
    3⟨IntDescriptor P⟩timeout
    4⟨IntDescriptor P⟩timeout
  72. print(f"Port: {config.port} (type: {type(config.port).__name__})")

    203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")
    outputPort: 8080 (type: int)
  73. print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name_…

    203print(f"Port: {config.port} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")205206# Practical example207print("\nPractical example:")208209# Database field descriptor210class Field:211    def __init__(self, name, field_type, required=False, default=None):212        self.name = name213        self.field_type = field_type214        self.required = required215        self.default = default216    217    def __get__(self, obj, type=None):218        if obj is None:219            return self220        return obj.__dict__.get(self.name, self.default)221    222    def __set__(self, obj, value):223        if value is None:224            if self.required:225                raise ValueError(f"{self.name} is required")226            obj.__dict__[self.name] = self.default227        elif not isinstance(value, self.field_type):228            raise TypeError(f"{self.name} must be {self.field_type.__name__}")229        else:230            obj.__dict__[self.name] = value231232class DatabaseRecord:233    id = Field('id', int, required=True)234    name = Field('name', str, required=True)
    outputTimeout: 30 (type: int)
    
    Practical example:
  74. self.name ← id, self.field_type ← <class 'int'>, self.required ← True

    pass 1 of 4
    210class Field:211    def __init__(self⟨Field R⟩, nameid, field_type<class 'int'>, requiredTrue=FalseFalse, defaultNone=NoneNone):212        self.name→ id = nameid213        self.field_type→ <class 'int'> = field_type<class 'int'>214        self.required→ True = requiredTrue215        self.default→ None = defaultNone
    All 4 passes — pass 1 is the card above
    passselfnamefield_typerequireddefaultself.nameself.field_typeself.requiredself.default
    1⟨Field R⟩id<class 'int'>TrueNoneid<class 'int'>TrueNone
    2⟨Field S⟩name<class 'str'>TrueNonename<class 'str'>TrueNone
    3⟨Field T⟩email<class 'str'>FalseNoneemail<class 'str'>FalseNone
    4⟨Field U⟩age<class 'int'>False0age<class 'int'>False0
  75. id ← <built-in function id>

    232class DatabaseRecord:233    id→ <built-in function id> = Field('id', int, required=True)234    name = Field('name', str, required=True)235    email = Field('email', str)
  76. name ← (empty)

    233id = Field('id', int, required=True)234name→ (empty) = Field('name', str, required=True)235email = Field('email', str)236age = Field('age', int, default=0)
  77. email ← (empty)

    234name = Field('name', str, required=True)235email→ (empty) = Field('email', str)236age = Field('age', int, default=0)
  78. age ← (empty)

    235email = Field('email', str)236age→ (empty) = Field('age', int, default=0)
  79. def __set__(self, obj, value):

    pass 1 of 4
    222def __set__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, value1):223    if value is None:224        if self.required:
    All 4 passes — pass 1 is the card above
    passselfobjvalue
    1⟨Field R⟩⟨DatabaseRecord V⟩1
    2⟨Field S⟩⟨DatabaseRecord V⟩Alice
    3⟨Field T⟩⟨DatabaseRecord V⟩alice@example.com
    4⟨Field S⟩⟨DatabaseRecord W⟩Bob
  80. obj.__dict__[self.name] ← 1

    pass 1 of 4
    227elif not isinstance(value, self.field_type):228    raise TypeError(f"{self.name} must be {self.field_type.__name__}")229else:230    obj.__dict__[self.name]→ 1 = value1
    All 4 passes — pass 1 is the card above
    passvalueobj.__dict__[self.name]
    111
    2AliceAlice
    3alice@example.comalice@example.com
    4BobBob
  81. print(f"Record: id={record.id}, name={record.name}, email={record.emai…

    243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")
  82. def __get__(self, obj, type=None):

    pass 1 of 4
    217def __get__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, type<class '__main__.DatabaseRecord'>=NoneNone):218    if obj is None:219        return self220    return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)
    All 4 passes — pass 1 is the card above
    passselfself.nameself.default
    1⟨Field R⟩idNone
    2⟨Field S⟩nameNone
    3⟨Field T⟩emailNone
    4⟨Field U⟩age0
  83. print(f"Record: id={record.id}, name={record.name}, email={record.emai…

    243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")
    outputRecord: id=1, name=Alice, email=alice@example.com, age=0
  84. record2 ← ⟨DatabaseRecord W⟩

    245try:246    record2→ ⟨DatabaseRecord W⟩ = DatabaseRecord()247    record2.name = "Bob"248    # Missing required 'id'
  85. record2.name ← Bob

    246record2 = DatabaseRecord()247record2.name→ Bob = "Bob"248# Missing required 'id'
  1. """Custom descriptor classes"""

    1"""Custom descriptor classes"""23# Typed descriptor4print("Typed descriptor:")56class TypedDescriptor:7    def __init__(self, name, expected_type):8        self.name = name9        self.expected_type = expected_type10    11    def __get__(self, obj, type=None):12        if obj is None:13            return self14        return obj.__dict__.get(self.name)15    16    def __set__(self, obj, value):17        if not isinstance(value, self.expected_type):18            raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19        obj.__dict__[self.name] = value2021class Person:22    name = TypedDescriptor('name', str)23    age = TypedDescriptor('age', int)
    outputTyped descriptor:
  2. self.name ← name, self.expected_type ← <class 'str'>

    pass 1 of 3
    6class TypedDescriptor:7    def __init__(self⟨TypedDescriptor A⟩, namename, expected_type<class 'str'>):8        self.name→ name = namename9        self.expected_type→ <class 'str'> = expected_type<class 'str'>
    All 3 passes — pass 1 is the card above
    passselfnameexpected_typeself.nameself.expected_type
    1⟨TypedDescriptor A⟩name<class 'str'>name<class 'str'>
    2⟨TypedDescriptor B⟩age<class 'int'>age<class 'int'>
    3⟨TypedDescriptor C⟩height<class 'float'>height<class 'float'>
  3. name ← (empty)

    21class Person:22    name→ (empty) = TypedDescriptor('name', str)23    age = TypedDescriptor('age', int)24    height = TypedDescriptor('height', float)
  4. age ← (empty)

    22name = TypedDescriptor('name', str)23age→ (empty) = TypedDescriptor('age', int)24height = TypedDescriptor('height', float)
  5. height ← (empty), person ← ⟨Person D⟩

    23    age = TypedDescriptor('age', int)24    height→ (empty) = TypedDescriptor('height', float)2526person→ ⟨Person D⟩ = Person()27person.name = "Alice"28person.age = 30
  6. obj.__dict__[self.name] ← Alice

    pass 1 of 4
    16def __set__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, valueAlice):17    if not isinstance(value, self.expected_type):18        raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19    obj.__dict__[self.name]→ Alice = valueAlice
    All 4 passes — pass 1 is the card above
    passselfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]
    1⟨TypedDescriptor A⟩AliceAlice
    2⟨TypedDescriptor B⟩3030
    3⟨TypedDescriptor C⟩1.651.65
    4⟨TypedDescriptor B⟩thirty<class 'int'>ageintage must be int, got str
  7. person.name ← Alice

    26person = Person()27person.name→ Alice = "Alice"28person.age = 3029person.height = 1.65
  8. person.age ← 30

    27person.name = "Alice"28person.age→ 30 = 3029person.height = 1.65
  9. person.height ← 1.65

    28person.age = 3029person.height→ 1.65 = 1.653031print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")
  10. def __get__(self, obj, type=None):

    pass 1 of 3
    11def __get__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, type<class '__main__.Person'>=NoneNone):12    if obj is None:13        return self14    return obj.__dict__{'name': 'Alice', 'age': 30, 'height': 1.65}.get(self.namename)
    All 3 passes — pass 1 is the card above
    passselfself.name
    1⟨TypedDescriptor A⟩name
    2⟨TypedDescriptor B⟩age
    3⟨TypedDescriptor C⟩height
  11. print(f"Person: {person.name}, {person.age}, {person.height}m")

    31print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")
    outputPerson: Alice, 30, 1.65m
  12. if not isinstance(value, self.expected_type):

    16def __set__(self, obj, value):17    if not isinstance(valuethirty, self.expected_type<class 'int'>):18        raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}, got {type(valuethirty).__name__}")19    obj.__dict__[self.name] = value
  13. except TypeError as e:

    34    person.age = "thirty"35except TypeError as e:36    print(f"Error: {eage must be int, got str}")
    outputError: age must be int, got str
  14. print(" Bounded descriptor:")

    38# Bounded descriptor39print("\nBounded descriptor:")4041class BoundedNumber:42    def __init__(self, name, min_value=None, max_value=None):43        self.name = name44        self.min_value = min_value45        self.max_value = max_value46    47    def __get__(self, obj, type=None):48        if obj is None:49            return self50        return obj.__dict__.get(self.name)51    52    def __set__(self, obj, value):53        if self.min_value is not None and value < self.min_value:54            raise ValueError(f"{self.name} must be >= {self.min_value}")55        if self.max_value is not None and value > self.max_value:56            raise ValueError(f"{self.name} must be <= {self.max_value}")57        obj.__dict__[self.name] = value5859class Score:60    value = BoundedNumber('value', min_value=0, max_value=100)
    output
    Bounded descriptor:
  15. self.name ← value, self.min_value ← 0, self.max_value ← 100

    41class BoundedNumber:42    def __init__(self⟨BoundedNumber E⟩, namevalue, min_value0=NoneNone, max_value100=None):43        self.name→ value = namevalue44        self.min_value→ 0 = min_value045        self.max_value→ 100 = max_value100
  16. value ← (empty), score ← ⟨Score F⟩, initial_score ← 60

    59class Score:60    value→ (empty) = BoundedNumber('value', min_value=0, max_value=100)6162score→ ⟨Score F⟩ = Score()63initial_score→ 60 = 6064score.value = initial_score6065print(f"Score: {score.value}")
  17. obj.__dict__[self.name] ← 60

    pass 1 of 2
    52def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value60):53    if self.min_value is not None and value < self.min_value:54        raise ValueError(f"{self.name} must be >= {self.min_value}")55    if self.max_value is not None and value > self.max_value:56        raise ValueError(f"{self.name} must be <= {self.max_value}")57    obj.__dict__[self.name]→ 60 = value60
  18. score.value ← 60

    63initial_score = 6064score.value→ 60 = initial_score6065print(f"Score: {score.value60}")
  19. def __get__(self, obj, type=None):

    47def __get__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, type<class '__main__.Score'>=NoneNone):48    if obj is None:49        return self50    return obj.__dict__{'value': 60}.get(self.namevalue)
  20. print(f"Score: {score.value}")

    64score.value = initial_score65print(f"Score: {score.value60}")
    outputScore: 60
  21. def __set__(self, obj, value):

    pass 2 of 2
    52def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value150):53    if self.min_value is not None and value < self.min_value:54        raise ValueError(f"{self.name} must be >= {self.min_value}")
  22. if self.max_value is not None and value > self.max_value:

    54    raise ValueError(f"{self.name} must be >= {self.min_value}")55if self.max_value100 is not None and value150 > self.max_value:56    raise ValueError(f"{self.namevalue} must be <= {self.max_value100}")57obj.__dict__[self.name] = value
  23. except ValueError as e:

    68    score.value = 15069except ValueError as e:70    print(f"Error: {evalue must be <= 100}")
    outputError: value must be <= 100
  24. print(" String descriptor:")

    72# String descriptor73print("\nString descriptor:")7475class ValidatedString:76    def __init__(self, name, min_length=0, max_length=None, pattern=None):77        self.name = name78        self.min_length = min_length79        self.max_length = max_length80        self.pattern = pattern81    82    def __get__(self, obj, type=None):83        if obj is None:84            return self85        return obj.__dict__.get(self.name)86    87    def __set__(self, obj, value):88        if not isinstance(value, str):89            raise TypeError(f"{self.name} must be a string")90        91        if len(value) < self.min_length:92            raise ValueError(f"{self.name} must be at least {self.min_length} characters")93        94        if self.max_length and len(value) > self.max_length:95            raise ValueError(f"{self.name} must be at most {self.max_length} characters")96        97        if self.pattern:98            import re99            if not re.match(self.pattern, value):100                raise ValueError(f"{self.name} doesn't match pattern")101        102        obj.__dict__[self.name] = value103104class User:105    username = ValidatedString('username', min_length=3, max_length=20)106    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
    output
    String descriptor:
  25. self.name ← username, self.min_length ← 3, self.max_length ← 20

    pass 1 of 2
    75class ValidatedString:76    def __init__(self⟨ValidatedString G⟩, nameusername, min_length3=0, max_length20=NoneNone, patternNone=None):77        self.name→ username = nameusername78        self.min_length→ 3 = min_length379        self.max_length→ 20 = max_length2080        self.pattern→ None = patternNone
  26. username ← (empty)

    104class User:105    username→ (empty) = ValidatedString('username', min_length=3, max_length=20)106    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
  27. self.name ← email, self.min_length ← 0, self.max_length ← None

    pass 2 of 2
    75class ValidatedString:76    def __init__(self⟨ValidatedString H⟩, nameemail, min_length0=0, max_lengthNone=NoneNone, pattern^[\w\.-]+@[\w\.-]+\.\w+$=None):77        self.name→ email = nameemail78        self.min_length→ 0 = min_length079        self.max_length→ None = max_lengthNone80        self.pattern→ ^[\w\.-]+@[\w\.-]+\.\w+$ = pattern^[\w\.-]+@[\w\.-]+\.\w+$
  28. email ← (empty), user ← ⟨User I⟩

    105    username = ValidatedString('username', min_length=3, max_length=20)106    email→ (empty) = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')107108user→ ⟨User I⟩ = User()109user.username = "alice"110user.email = "alice@example.com"
  29. obj.__dict__[self.name] ← alice

    pass 1 of 3
    87def __set__(self⟨ValidatedString G⟩, obj⟨User I⟩, valuealice):88    if not isinstance(value, str):89        raise TypeError(f"{self.name} must be a string")90    91    if len(value) < self.min_length:92        raise ValueError(f"{self.name} must be at least {self.min_length} characters")93    94    if self.max_length and len(value) > self.max_length:95        raise ValueError(f"{self.name} must be at most {self.max_length} characters")96    97    if self.pattern:98        import re99        if not re.match(self.pattern, value):100            raise ValueError(f"{self.name} doesn't match pattern")101    102    obj.__dict__[self.name]→ alice = valuealice
    All 3 passes — pass 1 is the card above
    passselfvalueself.patternself.min_lengthself.nameeobj.__dict__[self.name]
    1⟨ValidatedString G⟩alicealice
    2⟨ValidatedString H⟩alice@example.com^[\w\.-]+@[\w\.-]+\.\w+$
    3⟨ValidatedString G⟩ab3usernameusername must be at least 3 characters
  30. user.username ← alice

    108user = User()109user.username→ alice = "alice"110user.email = "alice@example.com"
  31. if self.pattern:

    97if self.pattern^[\w\.-]+@[\w\.-]+\.\w+$:98    import re99    if not re.match(self.pattern, value):
  32. obj.__dict__[self.name] ← alice@example.com

    102obj.__dict__[self.name]→ alice@example.com = valuealice@example.com
  33. user.email ← alice@example.com

    109user.username = "alice"110user.email→ alice@example.com = "alice@example.com"111112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")
  34. def __get__(self, obj, type=None):

    pass 1 of 2
    82def __get__(self⟨ValidatedString G⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83    if obj is None:84        return self85    return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameusername)
  35. def __get__(self, obj, type=None):

    pass 2 of 2
    82def __get__(self⟨ValidatedString H⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83    if obj is None:84        return self85    return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameemail)
  36. print(f"User: {user.username}, {user.email}")

    112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")
    outputUser: alice, alice@example.com
  37. if len(value) < self.min_length:

    91if len(valueab) < self.min_length3:92    raise ValueError(f"{self.nameusername} must be at least {self.min_length3} characters")
  38. except ValueError as e:

    115    user.username = "ab"  # Too short116except ValueError as e:117    print(f"Error: {eusername must be at least 3 characters}")
    outputError: username must be at least 3 characters
  39. print(" Choice descriptor:")

    119# Choice descriptor120print("\nChoice descriptor:")121122class Choice:123    def __init__(self, name, choices):124        self.name = name125        self.choices = choices126    127    def __get__(self, obj, type=None):128        if obj is None:129            return self130        return obj.__dict__.get(self.name)131    132    def __set__(self, obj, value):133        if value not in self.choices:134            raise ValueError(f"{self.name} must be one of {self.choices}")135        obj.__dict__[self.name] = value136137class Status:138    value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])
    output
    Choice descriptor:
  40. self.name ← value, self.choices ← ['pending', 'active', 'completed', 'cancelled']

    122class Choice:123    def __init__(self⟨Choice J⟩, namevalue, choices['pending', 'active', 'completed', 'cancelled']):124        self.name→ value = namevalue125        self.choices→ ['pending', 'active', 'completed', 'cancelled'] = choices['pending', 'active', 'completed', 'cancelled']
  41. value ← (empty), status ← ⟨Status K⟩

    137class Status:138    value→ (empty) = Choice('value', ['pending', 'active', 'completed', 'cancelled'])139140status→ ⟨Status K⟩ = Status()141status.value = 'active'142print(f"Status: {status.value}")
  42. obj.__dict__[self.name] ← active

    pass 1 of 2
    132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueactive):133    if value not in self.choices:134        raise ValueError(f"{self.name} must be one of {self.choices}")135    obj.__dict__[self.name]→ active = valueactive
  43. status.value ← active

    140status = Status()141status.value→ active = 'active'142print(f"Status: {status.valueactive}")
  44. def __get__(self, obj, type=None):

    127def __get__(self⟨Choice J⟩, obj⟨Status K⟩, type<class '__main__.Status'>=NoneNone):128    if obj is None:129        return self130    return obj.__dict__{'value': 'active'}.get(self.namevalue)
  45. print(f"Status: {status.value}")

    141status.value = 'active'142print(f"Status: {status.valueactive}")
    outputStatus: active
  46. def __set__(self, obj, value):

    pass 2 of 2
    132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueunknown):133    if value not in self.choices:134        raise ValueError(f"{self.name} must be one of {self.choices}")
  47. if value not in self.choices:

    132def __set__(self, obj, value):133    if valueunknown not in self.choices['pending', 'active', 'completed', 'cancelled']:134        raise ValueError(f"{self.namevalue} must be one of {self.choices['pending', 'active', 'completed', 'cancelled']}")135    obj.__dict__[self.name] = value
  48. except ValueError as e:

    145    status.value = 'unknown'146except ValueError as e:147    print(f"Error: {evalue must be one of ['pending', 'active', 'completed', 'cancelled']}")
    outputError: value must be one of ['pending', 'active', 'completed', 'cancelled']
  49. print(" Logged descriptor:")

    149# Logged descriptor150print("\nLogged descriptor:")151152class LoggedDescriptor:153    def __init__(self, name):154        self.name = name155    156    def __get__(self, obj, type=None):157        if obj is None:158            return self159        value = obj.__dict__.get(self.name)160        print(f"  Getting {self.name}: {value}")161        return value162    163    def __set__(self, obj, value):164        print(f"  Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")165        obj.__dict__[self.name] = value166167class TrackedObject:168    x = LoggedDescriptor('x')169    y = LoggedDescriptor('y')
    output
    Logged descriptor:
  50. self.name ← x

    pass 1 of 2
    152class LoggedDescriptor:153    def __init__(self⟨LoggedDescriptor L⟩, namex):154        self.name→ x = namex
  51. x ← (empty)

    167class TrackedObject:168    x→ (empty) = LoggedDescriptor('x')169    y = LoggedDescriptor('y')
  52. self.name ← y

    pass 2 of 2
    152class LoggedDescriptor:153    def __init__(self⟨LoggedDescriptor M⟩, namey):154        self.name→ y = namey
  53. y ← (empty), tracked ← ⟨TrackedObject N⟩

    168    x = LoggedDescriptor('x')169    y→ (empty) = LoggedDescriptor('y')170171tracked→ ⟨TrackedObject N⟩ = TrackedObject()172tracked.x = 10173tracked.y = 20
  54. obj.__dict__[self.name] ← 10

    pass 1 of 2
    163def __set__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, value10):164    print(f"  Setting {self.namex}: {obj.__dict__{}.get(self.name)} -> {value10}")165    obj.__dict__[self.name]→ 10 = value10
    output  Setting x: None -> 10
  55. tracked.x ← 10

    171tracked = TrackedObject()172tracked.x→ 10 = 10173tracked.y = 20174print(f"Sum: {tracked.x + tracked.y}")
  56. obj.__dict__[self.name] ← 20

    pass 2 of 2
    163def __set__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, value20):164    print(f"  Setting {self.namey}: {obj.__dict__{'x': 10}.get(self.name)} -> {value20}")165    obj.__dict__[self.name]→ 20 = value20
    output  Setting y: None -> 20
  57. tracked.y ← 20

    172tracked.x = 10173tracked.y→ 20 = 20174print(f"Sum: {tracked.x10 + tracked.y20}")
  58. value ← 10

    pass 1 of 2
    156def __get__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157    if obj is None:158        return self159    value→ 10 = obj.__dict__{'x': 10, 'y': 20}.get(self.namex)160    print(f"  Getting {self.namex}: {value10}")161    return value10
    output  Getting x: 10
  59. value ← 20

    pass 2 of 2
    156def __get__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157    if obj is None:158        return self159    value→ 20 = obj.__dict__{'x': 10, 'y': 20}.get(self.namey)160    print(f"  Getting {self.namey}: {value20}")161    return value20
    output  Getting y: 20
  60. print(f"Sum: {tracked.x + tracked.y}")

    173tracked.y = 20174print(f"Sum: {tracked.x10 + tracked.y20}")175176# Auto-converting descriptor177print("\nAuto-converting descriptor:")178179class IntDescriptor:180    def __init__(self, name):181        self.name = name182    183    def __get__(self, obj, type=None):184        if obj is None:185            return self186        return obj.__dict__.get(self.name)187    188    def __set__(self, obj, value):189        # Auto-convert to int190        try:191            obj.__dict__[self.name] = int(value)192        except (TypeError, ValueError) as e:193            raise ValueError(f"Cannot convert {value} to int")194195class Config:196    port = IntDescriptor('port')197    timeout = IntDescriptor('timeout')
    outputSum: 30
    
    Auto-converting descriptor:
  61. self.name ← port

    pass 1 of 2
    179class IntDescriptor:180    def __init__(self⟨IntDescriptor O⟩, nameport):181        self.name→ port = nameport
  62. port ← (empty)

    195class Config:196    port→ (empty) = IntDescriptor('port')197    timeout = IntDescriptor('timeout')
  63. self.name ← timeout

    pass 2 of 2
    179class IntDescriptor:180    def __init__(self⟨IntDescriptor P⟩, nametimeout):181        self.name→ timeout = nametimeout
  64. timeout ← (empty), config ← ⟨Config Q⟩

    196    port = IntDescriptor('port')197    timeout→ (empty) = IntDescriptor('timeout')198199config→ ⟨Config Q⟩ = Config()200config.port = "8080"  # String converted to int201config.timeout = 30.5  # Float converted to int
  65. def __set__(self, obj, value): # Auto-convert to int

    pass 1 of 2
    188def __set__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, value8080):189    # Auto-convert to int190    try:191        obj.__dict__[self.name] = int(value)
  66. obj.__dict__[self.name] ← 8080

    pass 1 of 2
    189# Auto-convert to int190try:191    obj.__dict__[self.name]→ 8080 = int(value8080)192except (TypeError, ValueError) as e:
  67. config.port ← 8080

    199config = Config()200config.port→ 8080 = "8080"  # String converted to int201config.timeout = 30.5  # Float converted to int
  68. def __set__(self, obj, value): # Auto-convert to int

    pass 2 of 2
    188def __set__(self⟨IntDescriptor P⟩, obj⟨Config Q⟩, value30.5):189    # Auto-convert to int190    try:191        obj.__dict__[self.name] = int(value)
  69. obj.__dict__[self.name] ← 30

    pass 2 of 2
    189# Auto-convert to int190try:191    obj.__dict__[self.name]→ 30 = int(value30.5)192except (TypeError, ValueError) as e:
  70. config.timeout ← 30

    200config.port = "8080"  # String converted to int201config.timeout→ 30 = 30.5  # Float converted to int202203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")
  71. def __get__(self, obj, type=None):

    pass 1 of 4
    183def __get__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, type<class '__main__.Config'>=NoneNone):184    if obj is None:185        return self186    return obj.__dict__{'port': 8080, 'timeout': 30}.get(self.nameport)
    All 4 passes — pass 1 is the card above
    passselfself.name
    1⟨IntDescriptor O⟩port
    2⟨IntDescriptor O⟩port
    3⟨IntDescriptor P⟩timeout
    4⟨IntDescriptor P⟩timeout
  72. print(f"Port: {config.port} (type: {type(config.port).__name__})")

    203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")
    outputPort: 8080 (type: int)
  73. print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name_…

    203print(f"Port: {config.port} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")205206# Practical example207print("\nPractical example:")208209# Database field descriptor210class Field:211    def __init__(self, name, field_type, required=False, default=None):212        self.name = name213        self.field_type = field_type214        self.required = required215        self.default = default216    217    def __get__(self, obj, type=None):218        if obj is None:219            return self220        return obj.__dict__.get(self.name, self.default)221    222    def __set__(self, obj, value):223        if value is None:224            if self.required:225                raise ValueError(f"{self.name} is required")226            obj.__dict__[self.name] = self.default227        elif not isinstance(value, self.field_type):228            raise TypeError(f"{self.name} must be {self.field_type.__name__}")229        else:230            obj.__dict__[self.name] = value231232class DatabaseRecord:233    id = Field('id', int, required=True)234    name = Field('name', str, required=True)
    outputTimeout: 30 (type: int)
    
    Practical example:
  74. self.name ← id, self.field_type ← <class 'int'>, self.required ← True

    pass 1 of 4
    210class Field:211    def __init__(self⟨Field R⟩, nameid, field_type<class 'int'>, requiredTrue=FalseFalse, defaultNone=NoneNone):212        self.name→ id = nameid213        self.field_type→ <class 'int'> = field_type<class 'int'>214        self.required→ True = requiredTrue215        self.default→ None = defaultNone
    All 4 passes — pass 1 is the card above
    passselfnamefield_typerequireddefaultself.nameself.field_typeself.requiredself.default
    1⟨Field R⟩id<class 'int'>TrueNoneid<class 'int'>TrueNone
    2⟨Field S⟩name<class 'str'>TrueNonename<class 'str'>TrueNone
    3⟨Field T⟩email<class 'str'>FalseNoneemail<class 'str'>FalseNone
    4⟨Field U⟩age<class 'int'>False0age<class 'int'>False0
  75. id ← <built-in function id>

    232class DatabaseRecord:233    id→ <built-in function id> = Field('id', int, required=True)234    name = Field('name', str, required=True)235    email = Field('email', str)
  76. name ← (empty)

    233id = Field('id', int, required=True)234name→ (empty) = Field('name', str, required=True)235email = Field('email', str)236age = Field('age', int, default=0)
  77. email ← (empty)

    234name = Field('name', str, required=True)235email→ (empty) = Field('email', str)236age = Field('age', int, default=0)
  78. age ← (empty)

    235email = Field('email', str)236age→ (empty) = Field('age', int, default=0)
  79. def __set__(self, obj, value):

    pass 1 of 4
    222def __set__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, value1):223    if value is None:224        if self.required:
    All 4 passes — pass 1 is the card above
    passselfobjvalue
    1⟨Field R⟩⟨DatabaseRecord V⟩1
    2⟨Field S⟩⟨DatabaseRecord V⟩Alice
    3⟨Field T⟩⟨DatabaseRecord V⟩alice@example.com
    4⟨Field S⟩⟨DatabaseRecord W⟩Bob
  80. obj.__dict__[self.name] ← 1

    pass 1 of 4
    227elif not isinstance(value, self.field_type):228    raise TypeError(f"{self.name} must be {self.field_type.__name__}")229else:230    obj.__dict__[self.name]→ 1 = value1
    All 4 passes — pass 1 is the card above
    passvalueobj.__dict__[self.name]
    111
    2AliceAlice
    3alice@example.comalice@example.com
    4BobBob
  81. print(f"Record: id={record.id}, name={record.name}, email={record.emai…

    243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")
  82. def __get__(self, obj, type=None):

    pass 1 of 4
    217def __get__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, type<class '__main__.DatabaseRecord'>=NoneNone):218    if obj is None:219        return self220    return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)
    All 4 passes — pass 1 is the card above
    passselfself.nameself.default
    1⟨Field R⟩idNone
    2⟨Field S⟩nameNone
    3⟨Field T⟩emailNone
    4⟨Field U⟩age0
  83. print(f"Record: id={record.id}, name={record.name}, email={record.emai…

    243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")
    outputRecord: id=1, name=Alice, email=alice@example.com, age=0
  84. record2 ← ⟨DatabaseRecord W⟩

    245try:246    record2→ ⟨DatabaseRecord W⟩ = DatabaseRecord()247    record2.name = "Bob"248    # Missing required 'id'
  85. record2.name ← Bob

    246record2 = DatabaseRecord()247record2.name→ Bob = "Bob"248# Missing required 'id'
  1. """Custom descriptor classes"""

    1"""Custom descriptor classes"""23# Typed descriptor4print("Typed descriptor:")56class TypedDescriptor:7    def __init__(self, name, expected_type):8        self.name = name9        self.expected_type = expected_type10    11    def __get__(self, obj, type=None):12        if obj is None:13            return self14        return obj.__dict__.get(self.name)15    16    def __set__(self, obj, value):17        if not isinstance(value, self.expected_type):18            raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19        obj.__dict__[self.name] = value2021class Person:22    name = TypedDescriptor('name', str)23    age = TypedDescriptor('age', int)
    outputTyped descriptor:
  2. self.name ← name, self.expected_type ← <class 'str'>

    pass 1 of 3
    6class TypedDescriptor:7    def __init__(self⟨TypedDescriptor A⟩, namename, expected_type<class 'str'>):8        self.name→ name = namename9        self.expected_type→ <class 'str'> = expected_type<class 'str'>
    All 3 passes — pass 1 is the card above
    passselfnameexpected_typeself.nameself.expected_type
    1⟨TypedDescriptor A⟩name<class 'str'>name<class 'str'>
    2⟨TypedDescriptor B⟩age<class 'int'>age<class 'int'>
    3⟨TypedDescriptor C⟩height<class 'float'>height<class 'float'>
  3. name ← (empty)

    21class Person:22    name→ (empty) = TypedDescriptor('name', str)23    age = TypedDescriptor('age', int)24    height = TypedDescriptor('height', float)
  4. age ← (empty)

    22name = TypedDescriptor('name', str)23age→ (empty) = TypedDescriptor('age', int)24height = TypedDescriptor('height', float)
  5. height ← (empty), person ← ⟨Person D⟩

    23    age = TypedDescriptor('age', int)24    height→ (empty) = TypedDescriptor('height', float)2526person→ ⟨Person D⟩ = Person()27person.name = "Alice"28person.age = 30
  6. obj.__dict__[self.name] ← Alice

    pass 1 of 4
    16def __set__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, valueAlice):17    if not isinstance(value, self.expected_type):18        raise TypeError(f"{self.name} must be {self.expected_type.__name__}, got {type(value).__name__}")19    obj.__dict__[self.name]→ Alice = valueAlice
    All 4 passes — pass 1 is the card above
    passselfvalueself.expected_typeself.nameself.expected_type.__name__eobj.__dict__[self.name]
    1⟨TypedDescriptor A⟩AliceAlice
    2⟨TypedDescriptor B⟩3030
    3⟨TypedDescriptor C⟩1.651.65
    4⟨TypedDescriptor B⟩thirty<class 'int'>ageintage must be int, got str
  7. person.name ← Alice

    26person = Person()27person.name→ Alice = "Alice"28person.age = 3029person.height = 1.65
  8. person.age ← 30

    27person.name = "Alice"28person.age→ 30 = 3029person.height = 1.65
  9. person.height ← 1.65

    28person.age = 3029person.height→ 1.65 = 1.653031print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")
  10. def __get__(self, obj, type=None):

    pass 1 of 3
    11def __get__(self⟨TypedDescriptor A⟩, obj⟨Person D⟩, type<class '__main__.Person'>=NoneNone):12    if obj is None:13        return self14    return obj.__dict__{'name': 'Alice', 'age': 30, 'height': 1.65}.get(self.namename)
    All 3 passes — pass 1 is the card above
    passselfself.name
    1⟨TypedDescriptor A⟩name
    2⟨TypedDescriptor B⟩age
    3⟨TypedDescriptor C⟩height
  11. print(f"Person: {person.name}, {person.age}, {person.height}m")

    31print(f"Person: {person.nameAlice}, {person.age30}, {person.height1.65}m")
    outputPerson: Alice, 30, 1.65m
  12. if not isinstance(value, self.expected_type):

    16def __set__(self, obj, value):17    if not isinstance(valuethirty, self.expected_type<class 'int'>):18        raise TypeError(f"{self.nameage} must be {self.expected_type.__name__int}, got {type(valuethirty).__name__}")19    obj.__dict__[self.name] = value
  13. except TypeError as e:

    34    person.age = "thirty"35except TypeError as e:36    print(f"Error: {eage must be int, got str}")
    outputError: age must be int, got str
  14. print(" Bounded descriptor:")

    38# Bounded descriptor39print("\nBounded descriptor:")4041class BoundedNumber:42    def __init__(self, name, min_value=None, max_value=None):43        self.name = name44        self.min_value = min_value45        self.max_value = max_value46    47    def __get__(self, obj, type=None):48        if obj is None:49            return self50        return obj.__dict__.get(self.name)51    52    def __set__(self, obj, value):53        if self.min_value is not None and value < self.min_value:54            raise ValueError(f"{self.name} must be >= {self.min_value}")55        if self.max_value is not None and value > self.max_value:56            raise ValueError(f"{self.name} must be <= {self.max_value}")57        obj.__dict__[self.name] = value5859class Score:60    value = BoundedNumber('value', min_value=0, max_value=100)
    output
    Bounded descriptor:
  15. self.name ← value, self.min_value ← 0, self.max_value ← 100

    41class BoundedNumber:42    def __init__(self⟨BoundedNumber E⟩, namevalue, min_value0=NoneNone, max_value100=None):43        self.name→ value = namevalue44        self.min_value→ 0 = min_value045        self.max_value→ 100 = max_value100
  16. value ← (empty), score ← ⟨Score F⟩, initial_score ← 100

    59class Score:60    value→ (empty) = BoundedNumber('value', min_value=0, max_value=100)6162score→ ⟨Score F⟩ = Score()63initial_score→ 100 = 10064score.value = initial_score10065print(f"Score: {score.value}")
  17. obj.__dict__[self.name] ← 100

    pass 1 of 2
    52def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value100):53    if self.min_value is not None and value < self.min_value:54        raise ValueError(f"{self.name} must be >= {self.min_value}")55    if self.max_value is not None and value > self.max_value:56        raise ValueError(f"{self.name} must be <= {self.max_value}")57    obj.__dict__[self.name]→ 100 = value100
  18. score.value ← 100

    63initial_score = 10064score.value→ 100 = initial_score10065print(f"Score: {score.value100}")
  19. def __get__(self, obj, type=None):

    47def __get__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, type<class '__main__.Score'>=NoneNone):48    if obj is None:49        return self50    return obj.__dict__{'value': 100}.get(self.namevalue)
  20. print(f"Score: {score.value}")

    64score.value = initial_score65print(f"Score: {score.value100}")
    outputScore: 100
  21. def __set__(self, obj, value):

    pass 2 of 2
    52def __set__(self⟨BoundedNumber E⟩, obj⟨Score F⟩, value150):53    if self.min_value is not None and value < self.min_value:54        raise ValueError(f"{self.name} must be >= {self.min_value}")
  22. if self.max_value is not None and value > self.max_value:

    54    raise ValueError(f"{self.name} must be >= {self.min_value}")55if self.max_value100 is not None and value150 > self.max_value:56    raise ValueError(f"{self.namevalue} must be <= {self.max_value100}")57obj.__dict__[self.name] = value
  23. except ValueError as e:

    68    score.value = 15069except ValueError as e:70    print(f"Error: {evalue must be <= 100}")
    outputError: value must be <= 100
  24. print(" String descriptor:")

    72# String descriptor73print("\nString descriptor:")7475class ValidatedString:76    def __init__(self, name, min_length=0, max_length=None, pattern=None):77        self.name = name78        self.min_length = min_length79        self.max_length = max_length80        self.pattern = pattern81    82    def __get__(self, obj, type=None):83        if obj is None:84            return self85        return obj.__dict__.get(self.name)86    87    def __set__(self, obj, value):88        if not isinstance(value, str):89            raise TypeError(f"{self.name} must be a string")90        91        if len(value) < self.min_length:92            raise ValueError(f"{self.name} must be at least {self.min_length} characters")93        94        if self.max_length and len(value) > self.max_length:95            raise ValueError(f"{self.name} must be at most {self.max_length} characters")96        97        if self.pattern:98            import re99            if not re.match(self.pattern, value):100                raise ValueError(f"{self.name} doesn't match pattern")101        102        obj.__dict__[self.name] = value103104class User:105    username = ValidatedString('username', min_length=3, max_length=20)106    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
    output
    String descriptor:
  25. self.name ← username, self.min_length ← 3, self.max_length ← 20

    pass 1 of 2
    75class ValidatedString:76    def __init__(self⟨ValidatedString G⟩, nameusername, min_length3=0, max_length20=NoneNone, patternNone=None):77        self.name→ username = nameusername78        self.min_length→ 3 = min_length379        self.max_length→ 20 = max_length2080        self.pattern→ None = patternNone
  26. username ← (empty)

    104class User:105    username→ (empty) = ValidatedString('username', min_length=3, max_length=20)106    email = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
  27. self.name ← email, self.min_length ← 0, self.max_length ← None

    pass 2 of 2
    75class ValidatedString:76    def __init__(self⟨ValidatedString H⟩, nameemail, min_length0=0, max_lengthNone=NoneNone, pattern^[\w\.-]+@[\w\.-]+\.\w+$=None):77        self.name→ email = nameemail78        self.min_length→ 0 = min_length079        self.max_length→ None = max_lengthNone80        self.pattern→ ^[\w\.-]+@[\w\.-]+\.\w+$ = pattern^[\w\.-]+@[\w\.-]+\.\w+$
  28. email ← (empty), user ← ⟨User I⟩

    105    username = ValidatedString('username', min_length=3, max_length=20)106    email→ (empty) = ValidatedString('email', pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')107108user→ ⟨User I⟩ = User()109user.username = "alice"110user.email = "alice@example.com"
  29. obj.__dict__[self.name] ← alice

    pass 1 of 3
    87def __set__(self⟨ValidatedString G⟩, obj⟨User I⟩, valuealice):88    if not isinstance(value, str):89        raise TypeError(f"{self.name} must be a string")90    91    if len(value) < self.min_length:92        raise ValueError(f"{self.name} must be at least {self.min_length} characters")93    94    if self.max_length and len(value) > self.max_length:95        raise ValueError(f"{self.name} must be at most {self.max_length} characters")96    97    if self.pattern:98        import re99        if not re.match(self.pattern, value):100            raise ValueError(f"{self.name} doesn't match pattern")101    102    obj.__dict__[self.name]→ alice = valuealice
    All 3 passes — pass 1 is the card above
    passselfvalueself.patternself.min_lengthself.nameeobj.__dict__[self.name]
    1⟨ValidatedString G⟩alicealice
    2⟨ValidatedString H⟩alice@example.com^[\w\.-]+@[\w\.-]+\.\w+$
    3⟨ValidatedString G⟩ab3usernameusername must be at least 3 characters
  30. user.username ← alice

    108user = User()109user.username→ alice = "alice"110user.email = "alice@example.com"
  31. if self.pattern:

    97if self.pattern^[\w\.-]+@[\w\.-]+\.\w+$:98    import re99    if not re.match(self.pattern, value):
  32. obj.__dict__[self.name] ← alice@example.com

    102obj.__dict__[self.name]→ alice@example.com = valuealice@example.com
  33. user.email ← alice@example.com

    109user.username = "alice"110user.email→ alice@example.com = "alice@example.com"111112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")
  34. def __get__(self, obj, type=None):

    pass 1 of 2
    82def __get__(self⟨ValidatedString G⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83    if obj is None:84        return self85    return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameusername)
  35. def __get__(self, obj, type=None):

    pass 2 of 2
    82def __get__(self⟨ValidatedString H⟩, obj⟨User I⟩, type<class '__main__.User'>=NoneNone):83    if obj is None:84        return self85    return obj.__dict__{'username': 'alice', 'email': 'alice@example.com'}.get(self.nameemail)
  36. print(f"User: {user.username}, {user.email}")

    112print(f"User: {user.usernamealice}, {user.emailalice@example.com}")
    outputUser: alice, alice@example.com
  37. if len(value) < self.min_length:

    91if len(valueab) < self.min_length3:92    raise ValueError(f"{self.nameusername} must be at least {self.min_length3} characters")
  38. except ValueError as e:

    115    user.username = "ab"  # Too short116except ValueError as e:117    print(f"Error: {eusername must be at least 3 characters}")
    outputError: username must be at least 3 characters
  39. print(" Choice descriptor:")

    119# Choice descriptor120print("\nChoice descriptor:")121122class Choice:123    def __init__(self, name, choices):124        self.name = name125        self.choices = choices126    127    def __get__(self, obj, type=None):128        if obj is None:129            return self130        return obj.__dict__.get(self.name)131    132    def __set__(self, obj, value):133        if value not in self.choices:134            raise ValueError(f"{self.name} must be one of {self.choices}")135        obj.__dict__[self.name] = value136137class Status:138    value = Choice('value', ['pending', 'active', 'completed', 'cancelled'])
    output
    Choice descriptor:
  40. self.name ← value, self.choices ← ['pending', 'active', 'completed', 'cancelled']

    122class Choice:123    def __init__(self⟨Choice J⟩, namevalue, choices['pending', 'active', 'completed', 'cancelled']):124        self.name→ value = namevalue125        self.choices→ ['pending', 'active', 'completed', 'cancelled'] = choices['pending', 'active', 'completed', 'cancelled']
  41. value ← (empty), status ← ⟨Status K⟩

    137class Status:138    value→ (empty) = Choice('value', ['pending', 'active', 'completed', 'cancelled'])139140status→ ⟨Status K⟩ = Status()141status.value = 'active'142print(f"Status: {status.value}")
  42. obj.__dict__[self.name] ← active

    pass 1 of 2
    132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueactive):133    if value not in self.choices:134        raise ValueError(f"{self.name} must be one of {self.choices}")135    obj.__dict__[self.name]→ active = valueactive
  43. status.value ← active

    140status = Status()141status.value→ active = 'active'142print(f"Status: {status.valueactive}")
  44. def __get__(self, obj, type=None):

    127def __get__(self⟨Choice J⟩, obj⟨Status K⟩, type<class '__main__.Status'>=NoneNone):128    if obj is None:129        return self130    return obj.__dict__{'value': 'active'}.get(self.namevalue)
  45. print(f"Status: {status.value}")

    141status.value = 'active'142print(f"Status: {status.valueactive}")
    outputStatus: active
  46. def __set__(self, obj, value):

    pass 2 of 2
    132def __set__(self⟨Choice J⟩, obj⟨Status K⟩, valueunknown):133    if value not in self.choices:134        raise ValueError(f"{self.name} must be one of {self.choices}")
  47. if value not in self.choices:

    132def __set__(self, obj, value):133    if valueunknown not in self.choices['pending', 'active', 'completed', 'cancelled']:134        raise ValueError(f"{self.namevalue} must be one of {self.choices['pending', 'active', 'completed', 'cancelled']}")135    obj.__dict__[self.name] = value
  48. except ValueError as e:

    145    status.value = 'unknown'146except ValueError as e:147    print(f"Error: {evalue must be one of ['pending', 'active', 'completed', 'cancelled']}")
    outputError: value must be one of ['pending', 'active', 'completed', 'cancelled']
  49. print(" Logged descriptor:")

    149# Logged descriptor150print("\nLogged descriptor:")151152class LoggedDescriptor:153    def __init__(self, name):154        self.name = name155    156    def __get__(self, obj, type=None):157        if obj is None:158            return self159        value = obj.__dict__.get(self.name)160        print(f"  Getting {self.name}: {value}")161        return value162    163    def __set__(self, obj, value):164        print(f"  Setting {self.name}: {obj.__dict__.get(self.name)} -> {value}")165        obj.__dict__[self.name] = value166167class TrackedObject:168    x = LoggedDescriptor('x')169    y = LoggedDescriptor('y')
    output
    Logged descriptor:
  50. self.name ← x

    pass 1 of 2
    152class LoggedDescriptor:153    def __init__(self⟨LoggedDescriptor L⟩, namex):154        self.name→ x = namex
  51. x ← (empty)

    167class TrackedObject:168    x→ (empty) = LoggedDescriptor('x')169    y = LoggedDescriptor('y')
  52. self.name ← y

    pass 2 of 2
    152class LoggedDescriptor:153    def __init__(self⟨LoggedDescriptor M⟩, namey):154        self.name→ y = namey
  53. y ← (empty), tracked ← ⟨TrackedObject N⟩

    168    x = LoggedDescriptor('x')169    y→ (empty) = LoggedDescriptor('y')170171tracked→ ⟨TrackedObject N⟩ = TrackedObject()172tracked.x = 10173tracked.y = 20
  54. obj.__dict__[self.name] ← 10

    pass 1 of 2
    163def __set__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, value10):164    print(f"  Setting {self.namex}: {obj.__dict__{}.get(self.name)} -> {value10}")165    obj.__dict__[self.name]→ 10 = value10
    output  Setting x: None -> 10
  55. tracked.x ← 10

    171tracked = TrackedObject()172tracked.x→ 10 = 10173tracked.y = 20174print(f"Sum: {tracked.x + tracked.y}")
  56. obj.__dict__[self.name] ← 20

    pass 2 of 2
    163def __set__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, value20):164    print(f"  Setting {self.namey}: {obj.__dict__{'x': 10}.get(self.name)} -> {value20}")165    obj.__dict__[self.name]→ 20 = value20
    output  Setting y: None -> 20
  57. tracked.y ← 20

    172tracked.x = 10173tracked.y→ 20 = 20174print(f"Sum: {tracked.x10 + tracked.y20}")
  58. value ← 10

    pass 1 of 2
    156def __get__(self⟨LoggedDescriptor L⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157    if obj is None:158        return self159    value→ 10 = obj.__dict__{'x': 10, 'y': 20}.get(self.namex)160    print(f"  Getting {self.namex}: {value10}")161    return value10
    output  Getting x: 10
  59. value ← 20

    pass 2 of 2
    156def __get__(self⟨LoggedDescriptor M⟩, obj⟨TrackedObject N⟩, type<class '__main__.TrackedObject'>=NoneNone):157    if obj is None:158        return self159    value→ 20 = obj.__dict__{'x': 10, 'y': 20}.get(self.namey)160    print(f"  Getting {self.namey}: {value20}")161    return value20
    output  Getting y: 20
  60. print(f"Sum: {tracked.x + tracked.y}")

    173tracked.y = 20174print(f"Sum: {tracked.x10 + tracked.y20}")175176# Auto-converting descriptor177print("\nAuto-converting descriptor:")178179class IntDescriptor:180    def __init__(self, name):181        self.name = name182    183    def __get__(self, obj, type=None):184        if obj is None:185            return self186        return obj.__dict__.get(self.name)187    188    def __set__(self, obj, value):189        # Auto-convert to int190        try:191            obj.__dict__[self.name] = int(value)192        except (TypeError, ValueError) as e:193            raise ValueError(f"Cannot convert {value} to int")194195class Config:196    port = IntDescriptor('port')197    timeout = IntDescriptor('timeout')
    outputSum: 30
    
    Auto-converting descriptor:
  61. self.name ← port

    pass 1 of 2
    179class IntDescriptor:180    def __init__(self⟨IntDescriptor O⟩, nameport):181        self.name→ port = nameport
  62. port ← (empty)

    195class Config:196    port→ (empty) = IntDescriptor('port')197    timeout = IntDescriptor('timeout')
  63. self.name ← timeout

    pass 2 of 2
    179class IntDescriptor:180    def __init__(self⟨IntDescriptor P⟩, nametimeout):181        self.name→ timeout = nametimeout
  64. timeout ← (empty), config ← ⟨Config Q⟩

    196    port = IntDescriptor('port')197    timeout→ (empty) = IntDescriptor('timeout')198199config→ ⟨Config Q⟩ = Config()200config.port = "8080"  # String converted to int201config.timeout = 30.5  # Float converted to int
  65. def __set__(self, obj, value): # Auto-convert to int

    pass 1 of 2
    188def __set__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, value8080):189    # Auto-convert to int190    try:191        obj.__dict__[self.name] = int(value)
  66. obj.__dict__[self.name] ← 8080

    pass 1 of 2
    189# Auto-convert to int190try:191    obj.__dict__[self.name]→ 8080 = int(value8080)192except (TypeError, ValueError) as e:
  67. config.port ← 8080

    199config = Config()200config.port→ 8080 = "8080"  # String converted to int201config.timeout = 30.5  # Float converted to int
  68. def __set__(self, obj, value): # Auto-convert to int

    pass 2 of 2
    188def __set__(self⟨IntDescriptor P⟩, obj⟨Config Q⟩, value30.5):189    # Auto-convert to int190    try:191        obj.__dict__[self.name] = int(value)
  69. obj.__dict__[self.name] ← 30

    pass 2 of 2
    189# Auto-convert to int190try:191    obj.__dict__[self.name]→ 30 = int(value30.5)192except (TypeError, ValueError) as e:
  70. config.timeout ← 30

    200config.port = "8080"  # String converted to int201config.timeout→ 30 = 30.5  # Float converted to int202203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name__})")
  71. def __get__(self, obj, type=None):

    pass 1 of 4
    183def __get__(self⟨IntDescriptor O⟩, obj⟨Config Q⟩, type<class '__main__.Config'>=NoneNone):184    if obj is None:185        return self186    return obj.__dict__{'port': 8080, 'timeout': 30}.get(self.nameport)
    All 4 passes — pass 1 is the card above
    passselfself.name
    1⟨IntDescriptor O⟩port
    2⟨IntDescriptor O⟩port
    3⟨IntDescriptor P⟩timeout
    4⟨IntDescriptor P⟩timeout
  72. print(f"Port: {config.port} (type: {type(config.port).__name__})")

    203print(f"Port: {config.port8080} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")
    outputPort: 8080 (type: int)
  73. print(f"Timeout: {config.timeout} (type: {type(config.timeout).__name_…

    203print(f"Port: {config.port} (type: {type(config.port).__name__})")204print(f"Timeout: {config.timeout30} (type: {type(config.timeout).__name__})")205206# Practical example207print("\nPractical example:")208209# Database field descriptor210class Field:211    def __init__(self, name, field_type, required=False, default=None):212        self.name = name213        self.field_type = field_type214        self.required = required215        self.default = default216    217    def __get__(self, obj, type=None):218        if obj is None:219            return self220        return obj.__dict__.get(self.name, self.default)221    222    def __set__(self, obj, value):223        if value is None:224            if self.required:225                raise ValueError(f"{self.name} is required")226            obj.__dict__[self.name] = self.default227        elif not isinstance(value, self.field_type):228            raise TypeError(f"{self.name} must be {self.field_type.__name__}")229        else:230            obj.__dict__[self.name] = value231232class DatabaseRecord:233    id = Field('id', int, required=True)234    name = Field('name', str, required=True)
    outputTimeout: 30 (type: int)
    
    Practical example:
  74. self.name ← id, self.field_type ← <class 'int'>, self.required ← True

    pass 1 of 4
    210class Field:211    def __init__(self⟨Field R⟩, nameid, field_type<class 'int'>, requiredTrue=FalseFalse, defaultNone=NoneNone):212        self.name→ id = nameid213        self.field_type→ <class 'int'> = field_type<class 'int'>214        self.required→ True = requiredTrue215        self.default→ None = defaultNone
    All 4 passes — pass 1 is the card above
    passselfnamefield_typerequireddefaultself.nameself.field_typeself.requiredself.default
    1⟨Field R⟩id<class 'int'>TrueNoneid<class 'int'>TrueNone
    2⟨Field S⟩name<class 'str'>TrueNonename<class 'str'>TrueNone
    3⟨Field T⟩email<class 'str'>FalseNoneemail<class 'str'>FalseNone
    4⟨Field U⟩age<class 'int'>False0age<class 'int'>False0
  75. id ← <built-in function id>

    232class DatabaseRecord:233    id→ <built-in function id> = Field('id', int, required=True)234    name = Field('name', str, required=True)235    email = Field('email', str)
  76. name ← (empty)

    233id = Field('id', int, required=True)234name→ (empty) = Field('name', str, required=True)235email = Field('email', str)236age = Field('age', int, default=0)
  77. email ← (empty)

    234name = Field('name', str, required=True)235email→ (empty) = Field('email', str)236age = Field('age', int, default=0)
  78. age ← (empty)

    235email = Field('email', str)236age→ (empty) = Field('age', int, default=0)
  79. def __set__(self, obj, value):

    pass 1 of 4
    222def __set__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, value1):223    if value is None:224        if self.required:
    All 4 passes — pass 1 is the card above
    passselfobjvalue
    1⟨Field R⟩⟨DatabaseRecord V⟩1
    2⟨Field S⟩⟨DatabaseRecord V⟩Alice
    3⟨Field T⟩⟨DatabaseRecord V⟩alice@example.com
    4⟨Field S⟩⟨DatabaseRecord W⟩Bob
  80. obj.__dict__[self.name] ← 1

    pass 1 of 4
    227elif not isinstance(value, self.field_type):228    raise TypeError(f"{self.name} must be {self.field_type.__name__}")229else:230    obj.__dict__[self.name]→ 1 = value1
    All 4 passes — pass 1 is the card above
    passvalueobj.__dict__[self.name]
    111
    2AliceAlice
    3alice@example.comalice@example.com
    4BobBob
  81. print(f"Record: id={record.id}, name={record.name}, email={record.emai…

    243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")
  82. def __get__(self, obj, type=None):

    pass 1 of 4
    217def __get__(self⟨Field R⟩, obj⟨DatabaseRecord V⟩, type<class '__main__.DatabaseRecord'>=NoneNone):218    if obj is None:219        return self220    return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)
    All 4 passes — pass 1 is the card above
    passselfself.nameself.default
    1⟨Field R⟩idNone
    2⟨Field S⟩nameNone
    3⟨Field T⟩emailNone
    4⟨Field U⟩age0
  83. print(f"Record: id={record.id}, name={record.name}, email={record.emai…

    243print(f"Record: id={record.id1}, name={record.nameAlice}, email={record.emailalice@example.com}, age={record.age0}")
    outputRecord: id=1, name=Alice, email=alice@example.com, age=0
  84. record2 ← ⟨DatabaseRecord W⟩

    245try:246    record2→ ⟨DatabaseRecord W⟩ = DatabaseRecord()247    record2.name = "Bob"248    # Missing required 'id'
  85. record2.name ← Bob

    246record2 = DatabaseRecord()247record2.name→ Bob = "Bob"248# Missing required 'id'

Custom descriptors shine when you need the same attribute behavior across many classes or attributes. They are the foundation of ORM field definitions.

custom descriptor Building reusable descriptor classes for validation, type checking, or other attribute behaviors that can be applied to multiple classes.

Data vs Non-data Descriptors

descriptor_data_vs_nondata.py
Replay: real traced execution (multi-file project)
"""Data vs non-data descriptors"""

# Non-data descriptor
print("Non-data descriptor:")

class NonDataDescriptor:
    def __get__(self, obj, type=None):
        return "From descriptor"

class TestClass:
    attr = NonDataDescriptor()

obj = TestClass()

# Get from descriptor
print(f"obj.attr: {obj.attr}")

# Set creates instance attribute
obj.attr = "From instance"
print(f"After setting: {obj.attr}")  # Instance wins

# Check __dict__
print(f"obj.__dict__: {obj.__dict__}")

# Data descriptor
print("\nData descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, "From descriptor")

    def __set__(self, obj, value):
        print(f"  Descriptor __set__ called")
        obj.__dict__[self.name] = value

class TestClass2:
    attr = DataDescriptor('attr')

obj2 = TestClass2()

# Get from descriptor
print(f"obj2.attr: {obj2.attr}")

# Set through descriptor
obj2.attr = "Value"
print(f"After setting: {obj2.attr}")

# Cannot bypass descriptor
obj2.__dict__['attr'] = "Direct"
print(f"obj2.attr (still via descriptor): {obj2.attr}")

# Lookup order
print("\nLookup order:")

class SimpleNonData:
    def __get__(self, obj, type=None):
        return "non-data descriptor"

class SimpleData:
    def __get__(self, obj, type=None):
        return "data descriptor"

    def __set__(self, obj, value):
        pass

class Demo:
    non_data = SimpleNonData()
    data = SimpleData()

demo = Demo()

print("Before instance attributes:")
print(f"  non_data: {demo.non_data}")
print(f"  data: {demo.data}")

# Add instance attributes
demo.__dict__['non_data'] = "instance value"
demo.__dict__['data'] = "instance value"

print("\nAfter instance attributes:")
print(f"  non_data: {demo.non_data}")  # Instance wins
print(f"  data: {demo.data}")  # Descriptor wins

print(f"\nInstance __dict__: {demo.__dict__}")

# Delete behavior
print("\nDelete behavior:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, "default")

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value

    def __delete__(self, obj):
        print(f"  Descriptor __delete__ called")
        if self.name in obj.__dict__:
            del obj.__dict__[self.name]

class Container:
    value = DeletableDescriptor('value')

container = Container()
container.value = 100
print(f"value: {container.value}")

# Delete through descriptor
del container.value
print(f"After delete: {container.value}")

# Method as non-data descriptor
print("\nMethod as non-data descriptor:")

class MethodDescriptor:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        # Return bound method
        return lambda *args, **kwargs: self.func(obj, *args, **kwargs)

class MyClass:
    def __init__(self, value):
        self.value = value

    @MethodDescriptor
    def get_value(self):
        return self.value

obj = MyClass(42)
print(f"get_value(): {obj.get_value()}")

# Overriding data descriptor
print("\nOverriding data descriptor:")

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

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError("Must be int")
        obj.__dict__[self.name] = value

class Numbers:
    value = StrictDescriptor('value')

nums = Numbers()
nums.value = 100
print(f"value: {nums.value}")

# Cannot override with direct assignment
try:
    nums.value = "text"
except TypeError as e:
    print(f"Error: {e}")

# Priority demonstration
print("\nPriority demonstration:")

print("Lookup priority:")
print("1. Data descriptors (from type(obj).__mro__)")
print("2. Instance attributes (from obj.__dict__)")
print("3. Non-data descriptors (from type(obj).__mro__)")
print("4. Class attributes")
print("5. __getattr__() if defined")

class ExampleNonData:
    def __get__(self, obj, type=None):
        return "non-data"

class ExampleData:
    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get('_data', 'data descriptor')

    def __set__(self, obj, value):
        obj.__dict__['_data'] = value

class PriorityDemo:
    non_data = ExampleNonData()
    data = ExampleData()
    class_attr = "class attribute"

demo = PriorityDemo()
print(f"\nInitial state:")
print(f"  non_data: {demo.non_data}")
print(f"  data: {demo.data}")
print(f"  class_attr: {demo.class_attr}")

# Add instance attributes
demo.__dict__['non_data'] = "instance non_data"
demo.__dict__['class_attr'] = "instance class_attr"

print(f"\nAfter adding instance attributes:")
print(f"  non_data: {demo.non_data}")  # instance wins (non-data)
print(f"  data: {demo.data}")  # descriptor wins (data)
print(f"  class_attr: {demo.class_attr}")  # instance wins (regular attr)

# Practical example
print("\nPractical example:")

# Validator that can be overridden
class Validator:
    def __init__(self, name, validate_func=None):
        self.name = name
        self.validate_func = validate_func

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if self.validate_func and not self.validate_func(value):
            raise ValueError(f"Invalid value for {self.name}")
        obj.__dict__[self.name] = value

class Product:
    price = Validator('price', lambda x: x >= 0)
    quantity = Validator('quantity', lambda x: x >= 0)

product = Product()
product.price = 19.99
product.quantity = 10

print(f"Product: price=${product.price}, quantity={product.quantity}")

try:
    product.price = -5
except ValueError as e:
    print(f"Error: {e}")

  1. attr ← (empty), obj ← ⟨TestClass A⟩

    1"""Data vs non-data descriptors"""23# Non-data descriptor4print("Non-data descriptor:")56class NonDataDescriptor:7    def __get__(self, obj, type=None):8        return "From descriptor"910class TestClass:11    attr→ (empty) = NonDataDescriptor()1213obj→ ⟨TestClass A⟩ = TestClass()1415# Get from descriptor16print(f"obj.attr: {obj.attrFrom descriptor}")
    outputNon-data descriptor:
  2. def __get__(self, obj, type=None):

    6class NonDataDescriptor:7    def __get__(self⟨NonDataDescriptor B⟩, obj⟨TestClass A⟩, type<class '__main__.TestClass'>=NoneNone):8        return "From descriptor"
  3. obj.attr ← From instance

    15# Get from descriptor16print(f"obj.attr: {obj.attrFrom descriptor}")1718# Set creates instance attribute19obj.attr→ From instance = "From instance"20print(f"After setting: {obj.attrFrom instance}")  # Instance wins2122# Check __dict__23print(f"obj.__dict__: {obj.__dict__{'attr': 'From instance'}}")2425# Data descriptor26print("\nData descriptor:")2728class DataDescriptor:29    def __init__(self, name):30        self.name = name31    32    def __get__(self, obj, type=None):33        if obj is None:34            return self35        return obj.__dict__.get(self.name, "From descriptor")36    37    def __set__(self, obj, value):38        print(f"  Descriptor __set__ called")39        obj.__dict__[self.name] = value4041class TestClass2:42    attr = DataDescriptor('attr')
    outputobj.attr: From descriptor
    After setting: From instance
    obj.__dict__: {'attr': 'From instance'}
    
    Data descriptor:
  4. self.name ← attr

    28class DataDescriptor:29    def __init__(self⟨DataDescriptor C⟩, nameattr):30        self.name→ attr = nameattr
  5. attr ← (empty), obj2 ← ⟨TestClass2 D⟩

    41class TestClass2:42    attr→ (empty) = DataDescriptor('attr')4344obj2→ ⟨TestClass2 D⟩ = TestClass2()4546# Get from descriptor47print(f"obj2.attr: {obj2.attrFrom descriptor}")
  6. def __get__(self, obj, type=None):

    pass 1 of 3
    32def __get__(self⟨DataDescriptor C⟩, obj⟨TestClass2 D⟩, type<class '__main__.TestClass2'>=NoneNone):33    if obj is None:34        return self35    return obj.__dict__{}.get(self.nameattr, "From descriptor")
    All 3 passes — pass 1 is the card above
    passobj.__dict__
    1{}
    2{'attr': 'Value'}
    3{'attr': 'Direct'}
  7. print(f"obj2.attr: {obj2.attr}")

    46# Get from descriptor47print(f"obj2.attr: {obj2.attrFrom descriptor}")4849# Set through descriptor50obj2.attr = "Value"51print(f"After setting: {obj2.attr}")
    outputobj2.attr: From descriptor
  8. obj.__dict__[self.name] ← Value

    37def __set__(self⟨DataDescriptor C⟩, obj⟨TestClass2 D⟩, valueValue):38    print(f"  Descriptor __set__ called")39    obj.__dict__[self.name]→ Value = valueValue
    output  Descriptor __set__ called
  9. obj2.attr ← Value

    49# Set through descriptor50obj2.attr→ Value = "Value"51print(f"After setting: {obj2.attrValue}")
  10. obj2.__dict__[’attr’] ← Direct

    50obj2.attr = "Value"51print(f"After setting: {obj2.attrValue}")5253# Cannot bypass descriptor54obj2.__dict__['attr']→ Direct = "Direct"55print(f"obj2.attr (still via descriptor): {obj2.attrDirect}")
    outputAfter setting: Value
  11. non_data ← (empty), data ← (empty), demo ← ⟨Demo E⟩

    54obj2.__dict__['attr'] = "Direct"55print(f"obj2.attr (still via descriptor): {obj2.attrDirect}")5657# Lookup order58print("\nLookup order:")5960class SimpleNonData:61    def __get__(self, obj, type=None):62        return "non-data descriptor"6364class SimpleData:65    def __get__(self, obj, type=None):66        return "data descriptor"67    68    def __set__(self, obj, value):69        pass7071class Demo:72    non_data→ (empty) = SimpleNonData()73    data→ (empty) = SimpleData()7475demo→ ⟨Demo E⟩ = Demo()7677print("Before instance attributes:")78print(f"  non_data: {demo.non_datanon-data descriptor}")79print(f"  data: {demo.data}")
    outputobj2.attr (still via descriptor): Direct
    
    Lookup order:
    Before instance attributes:
  12. def __get__(self, obj, type=None):

    60class SimpleNonData:61    def __get__(self⟨SimpleNonData F⟩, obj⟨Demo E⟩, type<class '__main__.Demo'>=NoneNone):62        return "non-data descriptor"
  13. print(f" non_data: {demo.non_data}")

    77print("Before instance attributes:")78print(f"  non_data: {demo.non_datanon-data descriptor}")79print(f"  data: {demo.datadata descriptor}")
    output  non_data: non-data descriptor
  14. def __get__(self, obj, type=None):

    pass 1 of 2
    64class SimpleData:65    def __get__(self⟨SimpleData G⟩, obj⟨Demo E⟩, type<class '__main__.Demo'>=NoneNone):66        return "data descriptor"
  15. demo.__dict__[’non_data’] ← instance value, demo.__dict__[’data’] ← instance value

    78print(f"  non_data: {demo.non_data}")79print(f"  data: {demo.datadata descriptor}")8081# Add instance attributes82demo.__dict__['non_data']→ instance value = "instance value"83demo.__dict__['data']→ instance value = "instance value"8485print("\nAfter instance attributes:")86print(f"  non_data: {demo.non_datainstance value}")  # Instance wins87print(f"  data: {demo.datadata descriptor}")  # Descriptor wins
    output  data: data descriptor
    
    After instance attributes:
      non_data: instance value
  16. def __get__(self, obj, type=None):

    pass 2 of 2
    64class SimpleData:65    def __get__(self⟨SimpleData G⟩, obj⟨Demo E⟩, type<class '__main__.Demo'>=NoneNone):66        return "data descriptor"
  17. print(f" data: {demo.data}") # Descriptor wins

    86print(f"  non_data: {demo.non_data}")  # Instance wins87print(f"  data: {demo.datadata descriptor}")  # Descriptor wins8889print(f"\nInstance __dict__: {demo.__dict__{'non_data': 'instance value', 'data': 'instance value'}}")9091# Delete behavior92print("\nDelete behavior:")9394class DeletableDescriptor:95    def __init__(self, name):96        self.name = name97    98    def __get__(self, obj, type=None):99        if obj is None:100            return self101        return obj.__dict__.get(self.name, "default")102    103    def __set__(self, obj, value):104        obj.__dict__[self.name] = value105    106    def __delete__(self, obj):107        print(f"  Descriptor __delete__ called")108        if self.name in obj.__dict__:109            del obj.__dict__[self.name]110111class Container:112    value = DeletableDescriptor('value')
    output  data: data descriptor
    
    Instance __dict__: {'non_data': 'instance value', 'data': 'instance value'}
    
    Delete behavior:
  18. self.name ← value

    94class DeletableDescriptor:95    def __init__(self⟨DeletableDescriptor H⟩, namevalue):96        self.name→ value = namevalue
  19. value ← (empty), container ← ⟨Container I⟩

    111class Container:112    value→ (empty) = DeletableDescriptor('value')113114container→ ⟨Container I⟩ = Container()115container.value = 100116print(f"value: {container.value}")
  20. obj.__dict__[self.name] ← 100

    103def __set__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩, value100):104    obj.__dict__[self.name]→ 100 = value100
  21. container.value ← 100

    114container = Container()115container.value→ 100 = 100116print(f"value: {container.value100}")
  22. def __get__(self, obj, type=None):

    pass 1 of 2
    98def __get__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩, type<class '__main__.Container'>=NoneNone):99    if obj is None:100        return self101    return obj.__dict__{'value': 100}.get(self.namevalue, "default")
  23. print(f"value: {container.value}")

    115container.value = 100116print(f"value: {container.value100}")
    outputvalue: 100
  24. def __delete__(self, obj):

    106def __delete__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩):107    print(f"  Descriptor __delete__ called")108    if self.name in obj.__dict__:
    output  Descriptor __delete__ called
  25. obj.__dict__[self.name] ← (empty)

    107print(f"  Descriptor __delete__ called")108if self.namevalue in obj.__dict__{'value': 100}:109    del obj.__dict__[self.name]→ (empty)
  26. print(f"After delete: {container.value}")

    119del container.value120print(f"After delete: {container.valuedefault}")
  27. def __get__(self, obj, type=None):

    pass 2 of 2
    98def __get__(self⟨DeletableDescriptor H⟩, obj⟨Container I⟩, type<class '__main__.Container'>=NoneNone):99    if obj is None:100        return self101    return obj.__dict__{}.get(self.namevalue, "default")
  28. print(f"After delete: {container.value}")

    119del container.value120print(f"After delete: {container.valuedefault}")121122# Method as non-data descriptor123print("\nMethod as non-data descriptor:")
    outputAfter delete: default
    
    Method as non-data descriptor:
  29. self.func ← ⟨function MyClass.get_value J⟩

    125class MethodDescriptor:126    def __init__(self⟨MethodDescriptor K⟩, func⟨function MyClass.get_value J⟩):127        self.func→ ⟨function MyClass.get_value J⟩ = func⟨function MyClass.get_value J⟩
  30. obj = MyClass(42)

    143obj = MyClass(42)144print(f"get_value(): {obj.get_value()}")
  31. self.value ← 42

    135class MyClass:136    def __init__(self⟨MyClass L⟩, value42):137        self.value→ 42 = value42
  32. obj ← ⟨MyClass L⟩

    143obj→ ⟨MyClass L⟩ = MyClass(42)144print(f"get_value(): {obj⟨MyClass L⟩.get_value()}")
  33. def __get__(self, obj, type=None):

    129def __get__(self⟨MethodDescriptor K⟩, obj⟨MyClass L⟩, type<class '__main__.MyClass'>=NoneNone):130    if obj is None:131        return self132    # Return bound method133    return lambda *args, **kwargs: self.func(obj, *args, **kwargs)
  34. def get_value(self):

    139@MethodDescriptor140def get_value(self⟨MyClass L⟩):141    return self.value42
  35. print(f"get_value(): {obj.get_value()}")

    143obj = MyClass(42)144print(f"get_value(): {obj⟨MyClass L⟩.get_value()}")145146# Overriding data descriptor147print("\nOverriding data descriptor:")148149class StrictDescriptor:150    def __init__(self, name):151        self.name = name152    153    def __get__(self, obj, type=None):154        if obj is None:155            return self156        return obj.__dict__.get(self.name)157    158    def __set__(self, obj, value):159        if not isinstance(value, int):160            raise TypeError("Must be int")161        obj.__dict__[self.name] = value162163class Numbers:164    value = StrictDescriptor('value')
    outputget_value(): 42
    
    Overriding data descriptor:
  36. self.name ← value

    149class StrictDescriptor:150    def __init__(self⟨StrictDescriptor A⟩, namevalue):151        self.name→ value = namevalue
  37. value ← (empty), nums ← ⟨Numbers M⟩

    163class Numbers:164    value→ (empty) = StrictDescriptor('value')165166nums→ ⟨Numbers M⟩ = Numbers()167nums.value = 100168print(f"value: {nums.value}")
  38. obj.__dict__[self.name] ← 100

    pass 1 of 2
    158def __set__(self⟨StrictDescriptor A⟩, obj⟨Numbers M⟩, value100):159    if not isinstance(value, int):160        raise TypeError("Must be int")161    obj.__dict__[self.name]→ 100 = value100
  39. nums.value ← 100

    166nums = Numbers()167nums.value→ 100 = 100168print(f"value: {nums.value100}")
  40. def __get__(self, obj, type=None):

    153def __get__(self⟨StrictDescriptor A⟩, obj⟨Numbers M⟩, type<class '__main__.Numbers'>=NoneNone):154    if obj is None:155        return self156    return obj.__dict__{'value': 100}.get(self.namevalue)
  41. print(f"value: {nums.value}")

    167nums.value = 100168print(f"value: {nums.value100}")
    outputvalue: 100
  42. def __set__(self, obj, value):

    pass 2 of 2
    158def __set__(self⟨StrictDescriptor A⟩, obj⟨Numbers M⟩, valuetext):159    if not isinstance(value, int):160        raise TypeError("Must be int")
  43. if not isinstance(value, int):

    158def __set__(self, obj, value):159    if not isinstance(valuetext, int):160        raise TypeError("Must be int")161    obj.__dict__[self.name] = value
  44. except TypeError as e:

    172    nums.value = "text"173except TypeError as e:174    print(f"Error: {eMust be int}")
    outputError: Must be int
  45. non_data ← (empty), data ← (empty), class_attr ← (empty), demo ← ⟨PriorityDemo N⟩

    176# Priority demonstration177print("\nPriority demonstration:")178179print("Lookup priority:")180print("1. Data descriptors (from type(obj).__mro__)")181print("2. Instance attributes (from obj.__dict__)")182print("3. Non-data descriptors (from type(obj).__mro__)")183print("4. Class attributes")184print("5. __getattr__() if defined")185186class ExampleNonData:187    def __get__(self, obj, type=None):188        return "non-data"189190class ExampleData:191    def __get__(self, obj, type=None):192        if obj is None:193            return self194        return obj.__dict__.get('_data', 'data descriptor')195    196    def __set__(self, obj, value):197        obj.__dict__['_data'] = value198199class PriorityDemo:200    non_data→ (empty) = ExampleNonData()201    data→ (empty) = ExampleData()202    class_attr→ (empty) = "class attribute"203204demo→ ⟨PriorityDemo N⟩ = PriorityDemo()205print(f"\nInitial state:")206print(f"  non_data: {demo.non_datanon-data}")207print(f"  data: {demo.data}")
    output
    Priority demonstration:
    Lookup priority:
    1. Data descriptors (from type(obj).__mro__)
    2. Instance attributes (from obj.__dict__)
    3. Non-data descriptors (from type(obj).__mro__)
    4. Class attributes
    5. __getattr__() if defined
    
    Initial state:
  46. def __get__(self, obj, type=None):

    186class ExampleNonData:187    def __get__(self⟨ExampleNonData O⟩, obj⟨PriorityDemo N⟩, type<class '__main__.PriorityDemo'>=NoneNone):188        return "non-data"
  47. print(f" non_data: {demo.non_data}")

    205print(f"\nInitial state:")206print(f"  non_data: {demo.non_datanon-data}")207print(f"  data: {demo.datadata descriptor}")208print(f"  class_attr: {demo.class_attr}")
    output  non_data: non-data
  48. def __get__(self, obj, type=None):

    pass 1 of 2
    190class ExampleData:191    def __get__(self⟨ExampleData P⟩, obj⟨PriorityDemo N⟩, type<class '__main__.PriorityDemo'>=NoneNone):192        if obj is None:193            return self194        return obj.__dict__{}.get('_data', 'data descriptor')
  49. demo.__dict__[’non_data’] ← instance non_data, demo.__dict__[’class_attr’] ← instance class_attr

    206print(f"  non_data: {demo.non_data}")207print(f"  data: {demo.datadata descriptor}")208print(f"  class_attr: {demo.class_attrclass attribute}")209210# Add instance attributes211demo.__dict__['non_data']→ instance non_data = "instance non_data"212demo.__dict__['class_attr']→ instance class_attr = "instance class_attr"213214print(f"\nAfter adding instance attributes:")215print(f"  non_data: {demo.non_datainstance non_data}")  # instance wins (non-data)216print(f"  data: {demo.datadata descriptor}")  # descriptor wins (data)217print(f"  class_attr: {demo.class_attr}")  # instance wins (regular attr)
    output  data: data descriptor
      class_attr: class attribute
    
    After adding instance attributes:
      non_data: instance non_data
  50. def __get__(self, obj, type=None):

    pass 2 of 2
    190class ExampleData:191    def __get__(self⟨ExampleData P⟩, obj⟨PriorityDemo N⟩, type<class '__main__.PriorityDemo'>=NoneNone):192        if obj is None:193            return self194        return obj.__dict__{'non_data': 'instance non_data', 'class_attr': 'instance class_attr'}.get('_data', 'data descriptor')
  51. print(f" data: {demo.data}") # descriptor wins (data)

    215print(f"  non_data: {demo.non_data}")  # instance wins (non-data)216print(f"  data: {demo.datadata descriptor}")  # descriptor wins (data)217print(f"  class_attr: {demo.class_attrinstance class_attr}")  # instance wins (regular attr)218219# Practical example220print("\nPractical example:")221222# Validator that can be overridden223class Validator:224    def __init__(self, name, validate_func=None):225        self.name = name226        self.validate_func = validate_func227    228    def __get__(self, obj, type=None):229        if obj is None:230            return self231        return obj.__dict__.get(self.name)232    233    def __set__(self, obj, value):234        if self.validate_func and not self.validate_func(value):235            raise ValueError(f"Invalid value for {self.name}")236        obj.__dict__[self.name] = value237238class Product:239    price = Validator('price', lambda x: x >= 0)240    quantity = Validator('quantity', lambda x: x >= 0)
    output  data: data descriptor
      class_attr: instance class_attr
    
    Practical example:
  52. self.name ← price, self.validate_func ← <function Product.<lambda> at ⟨addr Q⟩>

    pass 1 of 2
    223class Validator:224    def __init__(self⟨Validator E⟩, nameprice, validate_func<function Product.<lambda> at ⟨addr Q⟩>=NoneNone):225        self.name→ price = nameprice226        self.validate_func→ <function Product.<lambda> at ⟨addr Q⟩> = validate_func<function Product.<lambda> at ⟨addr Q⟩>
  53. price ← (empty)

    238class Product:239    price→ (empty) = Validator('price', lambda x: x >= 0)240    quantity = Validator('quantity', lambda x: x >= 0)
  54. self.name ← quantity, self.validate_func ← <function Product.<lambda> at ⟨addr R⟩>

    pass 2 of 2
    223class Validator:224    def __init__(self⟨Validator S⟩, namequantity, validate_func<function Product.<lambda> at ⟨addr R⟩>=NoneNone):225        self.name→ quantity = namequantity226        self.validate_func→ <function Product.<lambda> at ⟨addr R⟩> = validate_func<function Product.<lambda> at ⟨addr R⟩>
  55. quantity ← (empty), product ← ⟨Product T⟩

    239    price = Validator('price', lambda x: x >= 0)240    quantity→ (empty) = Validator('quantity', lambda x: x >= 0)241242product→ ⟨Product T⟩ = Product()243product.price = 19.99244product.quantity = 10
  56. obj.__dict__[self.name] ← 19.99

    pass 1 of 3
    233def __set__(self⟨Validator E⟩, obj⟨Product T⟩, value19.99):234    if self.validate_func and not self.validate_func(value):235        raise ValueError(f"Invalid value for {self.name}")236    obj.__dict__[self.name]→ 19.99 = value19.99
    All 3 passes — pass 1 is the card above
    passselfvalueself.validate_funcself.nameeobj.__dict__[self.name]
    1⟨Validator E⟩19.9919.99
    2⟨Validator S⟩1010
    3⟨Validator E⟩-5<function Product.<lambda> at ⟨addr Q⟩>priceInvalid value for price
  57. product.price ← 19.99

    242product = Product()243product.price→ 19.99 = 19.99244product.quantity = 10
  58. product.quantity ← 10

    243product.price = 19.99244product.quantity→ 10 = 10245246print(f"Product: price=${product.price19.99}, quantity={product.quantity10}")
  59. def __get__(self, obj, type=None):

    pass 1 of 2
    228def __get__(self⟨Validator E⟩, obj⟨Product T⟩, type<class '__main__.Product'>=NoneNone):229    if obj is None:230        return self231    return obj.__dict__{'price': 19.99, 'quantity': 10}.get(self.nameprice)
  60. def __get__(self, obj, type=None):

    pass 2 of 2
    228def __get__(self⟨Validator S⟩, obj⟨Product T⟩, type<class '__main__.Product'>=NoneNone):229    if obj is None:230        return self231    return obj.__dict__{'price': 19.99, 'quantity': 10}.get(self.namequantity)
  61. print(f"Product: price=${product.price}, quantity={product.quantity}")

    246print(f"Product: price=${product.price19.99}, quantity={product.quantity10}")
    outputProduct: price=$19.99, quantity=10
  62. if self.validate_func and not self.validate_func(value):

    233def __set__(self, obj, value):234    if self.validate_func<function Product.<lambda> at ⟨addr Q⟩> and not self.validate_func(value-5):235        raise ValueError(f"Invalid value for {self.nameprice}")236    obj.__dict__[self.name] = value
  63. except ValueError as e:

    249    product.price = -5250except ValueError as e:251    print(f"Error: {eInvalid value for price}")
    outputError: Invalid value for price

This distinction is crucial: data descriptors cannot be shadowed by instance attributes, making them ideal for validation. Non-data descriptors can be overridden, which is how method caching works.

descriptor types Data descriptors (with `__set__` or `__delete__`) take priority over instance attributes; non-data descriptors (only `__get__`) do not.

Practical Applications

descriptor_practical.py
Replay: real traced execution (multi-file project)
"""Practical descriptor applications"""

# ORM-style field
print("ORM-style field:")

class Field:
    def __init__(self, field_type, default=None):
        self.field_type = field_type
        self.default = default
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, self.default)

    def __set__(self, obj, value):
        if value is not None and not isinstance(value, self.field_type):
            raise TypeError(f"{self.name} must be {self.field_type.__name__}")
        obj.__dict__[self.name] = value

class User:
    id = Field(int)
    name = Field(str)
    email = Field(str)
    age = Field(int, default=0)

user = User()
user.id = 1
user.name = "Alice"
user.email = "alice@example.com"

print(f"User: id={user.id}, name={user.name}, email={user.email}, age={user.age}")

# Lazy loading
print("\nLazy loading:")

class LazyLoad:
    def __init__(self, loader_func):
        self.loader_func = loader_func
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self

        # Check if already loaded
        cache_name = f'_lazy_{self.name}'
        if cache_name not in obj.__dict__:
            print(f"  Loading {self.name}...")
            obj.__dict__[cache_name] = self.loader_func(obj)

        return obj.__dict__[cache_name]

class Report:
    def __init__(self, report_id):
        self.report_id = report_id

    @LazyLoad
    def data(self):
        # Expensive operation
        return f"Report data for {self.report_id}"

    @LazyLoad
    def statistics(self):
        # Another expensive operation
        return {"count": 100, "avg": 50}

report = Report(123)
print("Report created (data not loaded)")

print("\nFirst access to data:")
print(report.data)

print("\nSecond access (cached):")
print(report.data)

print("\nFirst access to statistics:")
print(report.statistics)

# Validation with __set_name__
print("\nValidation with __set_name__:")

class Validated:
    def __init__(self, validator):
        self.validator = validator
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not self.validator(value):
            raise ValueError(f"Invalid value for {self.name}: {value}")
        obj.__dict__[self.name] = value

class Account:
    balance = Validated(lambda x: x >= 0)
    interest_rate = Validated(lambda x: 0 <= x <= 1)

account = Account()
account.balance = 1000
account.interest_rate = 0.05

print(f"Account: balance={account.balance}, interest_rate={account.interest_rate}")

try:
    account.balance = -100
except ValueError as e:
    print(f"Error: {e}")

# Tracked attributes
print("\nTracked attributes:")

class Tracked:
    def __init__(self):
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        old_value = obj.__dict__.get(self.name)
        obj.__dict__[self.name] = value

        # Track changes
        if not hasattr(obj, '_changes'):
            obj._changes = []

        obj._changes.append({
            'field': self.name,
            'old': old_value,
            'new': value
        })

class TrackedModel:
    name = Tracked()
    value = Tracked()

model = TrackedModel()
model.name = "Initial"
model.value = 100
model.name = "Updated"
model.value = 200

print("Changes:")
for change in model._changes:
    print(f"  {change['field']}: {change['old']} -> {change['new']}")

# Type conversion
print("\nType conversion:")

class Converted:
    def __init__(self, converter):
        self.converter = converter
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        converted = self.converter(value)
        obj.__dict__[self.name] = converted

class Config:
    port = Converted(int)
    debug = Converted(bool)
    timeout = Converted(float)

config = Config()
config.port = "8080"
config.debug = "yes"
config.timeout = "30.5"

print(f"Config: port={config.port} ({type(config.port).__name__})")
print(f"        debug={config.debug} ({type(config.debug).__name__})")
print(f"        timeout={config.timeout} ({type(config.timeout).__name__})")

# Units descriptor
print("\nUnits descriptor:")

class Quantity:
    def __init__(self, unit):
        self.unit = unit
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.name, 0)
        return f"{value} {self.unit}"

    def __set__(self, obj, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f"{self.name} must be numeric")
        obj.__dict__[self.name] = value

class Product:
    weight = Quantity("kg")
    length = Quantity("cm")
    price = Quantity("USD")

product = Product()
product.weight = 2.5
product.length = 30
product.price = 19.99

print(f"Product: weight={product.weight}, length={product.length}, price={product.price}")

# Practical example
print("\nPractical example:")

# Complete model with multiple descriptor types
class IntField:
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError(f"{self.name} must be int")
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} must be >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} must be <= {self.max_value}")
        obj.__dict__[self.name] = value

class StringField:
    def __init__(self, max_length=None):
        self.max_length = max_length
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name, "")

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError(f"{self.name} must be str")
        if self.max_length and len(value) > self.max_length:
            raise ValueError(f"{self.name} too long (max {self.max_length})")
        obj.__dict__[self.name] = value

class Student:
    id = IntField(min_value=1)
    name = StringField(max_length=50)
    age = IntField(min_value=0, max_value=150)
    grade = IntField(min_value=0, max_value=100)

student = Student()
student.id = 1
student.name = "Alice"
student.age = 20
student.grade = 85

print(f"Student: id={student.id}, name={student.name}, age={student.age}, grade={student.grade}")

# Validation works
try:
    student.grade = 105
except ValueError as e:
    print(f"Error: {e}")

  1. """Practical descriptor applications"""

    1"""Practical descriptor applications"""23# ORM-style field4print("ORM-style field:")56class Field:7    def __init__(self, field_type, default=None):8        self.field_type = field_type9        self.default = default10        self.name = None11    12    def __set_name__(self, owner, name):13        self.name = name14    15    def __get__(self, obj, type=None):16        if obj is None:17            return self18        return obj.__dict__.get(self.name, self.default)19    20    def __set__(self, obj, value):21        if value is not None and not isinstance(value, self.field_type):22            raise TypeError(f"{self.name} must be {self.field_type.__name__}")23        obj.__dict__[self.name] = value2425class User:26    id = Field(int)27    name = Field(str)
    outputORM-style field:
  2. self.field_type ← <class 'int'>, self.default ← None, self.name ← None

    pass 1 of 4
    6class Field:7    def __init__(self⟨Field A⟩, field_type<class 'int'>, defaultNone=NoneNone):8        self.field_type→ <class 'int'> = field_type<class 'int'>9        self.default→ None = defaultNone10        self.name→ None = None
    All 4 passes — pass 1 is the card above
    passselffield_typedefaultself.field_typeself.defaultself.name
    1⟨Field A⟩<class 'int'>None<class 'int'>NoneNone
    2⟨Field B⟩<class 'str'>None<class 'str'>NoneNone
    3⟨Field C⟩<class 'str'>None<class 'str'>NoneNone
    4⟨Field D⟩<class 'int'>0<class 'int'>0None
  3. id ← <built-in function id>

    25class User:26    id→ <built-in function id> = Field(int)27    name = Field(str)28    email = Field(str)
  4. name ← (empty)

    26id = Field(int)27name→ (empty) = Field(str)28email = Field(str)29age = Field(int, default=0)
  5. email ← (empty)

    27name = Field(str)28email→ (empty) = Field(str)29age = Field(int, default=0)
  6. age ← (empty)

    28email = Field(str)29age→ (empty) = Field(int, default=0)
  7. self.name ← id

    pass 1 of 4
    12def __set_name__(self⟨Field A⟩, owner<class '__main__.User'>, nameid):13    self.name→ id = nameid
    All 4 passes — pass 1 is the card above
    passselfnameself.name
    1⟨Field A⟩idid
    2⟨Field B⟩namename
    3⟨Field C⟩emailemail
    4⟨Field D⟩ageage
  8. user ← ⟨User E⟩

    31user→ ⟨User E⟩ = User()32user.id = 133user.name = "Alice"
  9. obj.__dict__[self.name] ← 1

    pass 1 of 3
    20def __set__(self⟨Field A⟩, obj⟨User E⟩, value1):21    if value is not None and not isinstance(value, self.field_type):22        raise TypeError(f"{self.name} must be {self.field_type.__name__}")23    obj.__dict__[self.name]→ 1 = value1
    All 3 passes — pass 1 is the card above
    passselfvalueobj.__dict__[self.name]
    1⟨Field A⟩11
    2⟨Field B⟩AliceAlice
    3⟨Field C⟩alice@example.comalice@example.com
  10. user.id ← 1

    31user = User()32user.id→ 1 = 133user.name = "Alice"34user.email = "alice@example.com"
  11. user.name ← Alice

    32user.id = 133user.name→ Alice = "Alice"34user.email = "alice@example.com"
  12. user.email ← alice@example.com

    33user.name = "Alice"34user.email→ alice@example.com = "alice@example.com"3536print(f"User: id={user.id1}, name={user.nameAlice}, email={user.emailalice@example.com}, age={user.age0}")
  13. def __get__(self, obj, type=None):

    pass 1 of 4
    15def __get__(self⟨Field A⟩, obj⟨User E⟩, type<class '__main__.User'>=NoneNone):16    if obj is None:17        return self18    return obj.__dict__{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}.get(self.nameid, self.defaultNone)
    All 4 passes — pass 1 is the card above
    passselfself.nameself.default
    1⟨Field A⟩idNone
    2⟨Field B⟩nameNone
    3⟨Field C⟩emailNone
    4⟨Field D⟩age0
  14. print(f"User: id={user.id}, name={user.name}, email={user.email}, age=…

    36print(f"User: id={user.id1}, name={user.nameAlice}, email={user.emailalice@example.com}, age={user.age0}")3738# Lazy loading39print("\nLazy loading:")
    outputUser: id=1, name=Alice, email=alice@example.com, age=0
    
    Lazy loading:
  15. self.loader_func ← ⟨function Report.data F⟩, self.name ← None

    pass 1 of 2
    41class LazyLoad:42    def __init__(self⟨LazyLoad G⟩, loader_func⟨function Report.data F⟩):43        self.loader_func→ ⟨function Report.data F⟩ = loader_func⟨function Report.data F⟩44        self.name→ None = None
  16. self.loader_func ← ⟨function Report.statistics H⟩, self.name ← None

    pass 2 of 2
    41class LazyLoad:42    def __init__(self⟨LazyLoad I⟩, loader_func⟨function Report.statistics H⟩):43        self.loader_func→ ⟨function Report.statistics H⟩ = loader_func⟨function Report.statistics H⟩44        self.name→ None = None
  17. self.name ← data

    pass 1 of 2
    46def __set_name__(self⟨LazyLoad G⟩, owner<class '__main__.Report'>, namedata):47    self.name→ data = namedata
  18. self.name ← statistics

    pass 2 of 2
    46def __set_name__(self⟨LazyLoad I⟩, owner<class '__main__.Report'>, namestatistics):47    self.name→ statistics = namestatistics
  19. report = Report(123)

    75report = Report(123)76print("Report created (data not loaded)")
  20. self.report_id ← 123

    61class Report:62    def __init__(self⟨Report J⟩, report_id123):63        self.report_id→ 123 = report_id123
  21. report ← ⟨Report J⟩

    75report→ ⟨Report J⟩ = Report(123)76print("Report created (data not loaded)")7778print("\nFirst access to data:")79print(report.dataReport data for 123)
    outputReport created (data not loaded)
    
    First access to data:
  22. cache_name ← _lazy_data

    pass 1 of 3
    49def __get__(self⟨LazyLoad G⟩, obj⟨Report J⟩, type<class '__main__.Report'>=NoneNone):50    if obj is None:51        return self52    53    # Check if already loaded54    cache_name→ _lazy_data = f'_lazy_{self.namedata}'55    if cache_name not in obj.__dict__:56        print(f"  Loading {self.name}...")57        obj.__dict__[cache_name] = self.loader_func(obj)58    59    return obj.__dict__[cache_name]Report data for 123
    All 3 passes — pass 1 is the card above
    passselfself.nameobj.__dict__[cache_name]cache_name
    1⟨LazyLoad G⟩dataReport data for 123_lazy_data
    2⟨LazyLoad G⟩dataReport data for 123_lazy_data
    3⟨LazyLoad I⟩statistics{'count': 100, 'avg': 50}_lazy_statistics
  23. print(report.data)

    78print("\nFirst access to data:")79print(report.dataReport data for 123)8081print("\nSecond access (cached):")82print(report.dataReport data for 123)
    outputReport data for 123
    
    Second access (cached):
  24. print(report.data)

    81print("\nSecond access (cached):")82print(report.dataReport data for 123)8384print("\nFirst access to statistics:")85print(report.statistics{'count': 100, 'avg': 50})
    outputReport data for 123
    
    First access to statistics:
  25. print(report.statistics)

    84print("\nFirst access to statistics:")85print(report.statistics{'count': 100, 'avg': 50})8687# Validation with __set_name__88print("\nValidation with __set_name__:")8990class Validated:91    def __init__(self, validator):92        self.validator = validator93        self.name = None94    95    def __set_name__(self, owner, name):96        self.name = name97    98    def __get__(self, obj, type=None):99        if obj is None:100            return self101        return obj.__dict__.get(self.name)102    103    def __set__(self, obj, value):104        if not self.validator(value):105            raise ValueError(f"Invalid value for {self.name}: {value}")106        obj.__dict__[self.name] = value107108class Account:109    balance = Validated(lambda x: x >= 0)110    interest_rate = Validated(lambda x: 0 <= x <= 1)
    output{'count': 100, 'avg': 50}
    
    Validation with __set_name__:
  26. self.validator ← <function Account.<lambda> at ⟨addr K⟩>, self.name ← None

    pass 1 of 2
    90class Validated:91    def __init__(self⟨Validated L⟩, validator<function Account.<lambda> at ⟨addr K⟩>):92        self.validator→ <function Account.<lambda> at ⟨addr K⟩> = validator<function Account.<lambda> at ⟨addr K⟩>93        self.name→ None = None
  27. balance ← (empty)

    108class Account:109    balance→ (empty) = Validated(lambda x: x >= 0)110    interest_rate = Validated(lambda x: 0 <= x <= 1)
  28. self.validator ← <function Account.<lambda> at ⟨addr M⟩>, self.name ← None

    pass 2 of 2
    90class Validated:91    def __init__(self⟨Validated N⟩, validator<function Account.<lambda> at ⟨addr M⟩>):92        self.validator→ <function Account.<lambda> at ⟨addr M⟩> = validator<function Account.<lambda> at ⟨addr M⟩>93        self.name→ None = None
  29. interest_rate ← (empty)

    109balance = Validated(lambda x: x >= 0)110interest_rate→ (empty) = Validated(lambda x: 0 <= x <= 1)
  30. self.name ← balance

    pass 1 of 2
    95def __set_name__(self⟨Validated L⟩, owner<class '__main__.Account'>, namebalance):96    self.name→ balance = namebalance
  31. self.name ← interest_rate

    pass 2 of 2
    95def __set_name__(self⟨Validated N⟩, owner<class '__main__.Account'>, nameinterest_rate):96    self.name→ interest_rate = nameinterest_rate
  32. account ← ⟨Account O⟩

    112account→ ⟨Account O⟩ = Account()113account.balance = 1000114account.interest_rate = 0.05
  33. obj.__dict__[self.name] ← 1000

    pass 1 of 3
    103def __set__(self⟨Validated L⟩, obj⟨Account O⟩, value1000):104    if not self.validator(value):105        raise ValueError(f"Invalid value for {self.name}: {value}")106    obj.__dict__[self.name]→ 1000 = value1000
    All 3 passes — pass 1 is the card above
    passselfvalueself.nameeobj.__dict__[self.name]
    1⟨Validated L⟩10001000
    2⟨Validated N⟩0.050.05
    3⟨Validated L⟩-100balanceInvalid value for balance: -100
  34. account.balance ← 1000

    112account = Account()113account.balance→ 1000 = 1000114account.interest_rate = 0.05
  35. account.interest_rate ← 0.05

    113account.balance = 1000114account.interest_rate→ 0.05 = 0.05115116print(f"Account: balance={account.balance1000}, interest_rate={account.interest_rate0.05}")
  36. def __get__(self, obj, type=None):

    pass 1 of 2
    98def __get__(self⟨Validated L⟩, obj⟨Account O⟩, type<class '__main__.Account'>=NoneNone):99    if obj is None:100        return self101    return obj.__dict__{'balance': 1000, 'interest_rate': 0.05}.get(self.namebalance)
  37. def __get__(self, obj, type=None):

    pass 2 of 2
    98def __get__(self⟨Validated N⟩, obj⟨Account O⟩, type<class '__main__.Account'>=NoneNone):99    if obj is None:100        return self101    return obj.__dict__{'balance': 1000, 'interest_rate': 0.05}.get(self.nameinterest_rate)
  38. print(f"Account: balance={account.balance}, interest_rate={account.int…

    116print(f"Account: balance={account.balance1000}, interest_rate={account.interest_rate0.05}")
    outputAccount: balance=1000, interest_rate=0.05
  39. if not self.validator(value):

    103def __set__(self, obj, value):104    if not self.validator(value-100):105        raise ValueError(f"Invalid value for {self.namebalance}: {value-100}")106    obj.__dict__[self.name] = value
  40. except ValueError as e:

    119    account.balance = -100120except ValueError as e:121    print(f"Error: {eInvalid value for balance: -100}")
    outputError: Invalid value for balance: -100
  41. print(" Tracked attributes:")

    123# Tracked attributes124print("\nTracked attributes:")125126class Tracked:127    def __init__(self):128        self.name = None129    130    def __set_name__(self, owner, name):131        self.name = name132    133    def __get__(self, obj, type=None):134        if obj is None:135            return self136        return obj.__dict__.get(self.name)137    138    def __set__(self, obj, value):139        old_value = obj.__dict__.get(self.name)140        obj.__dict__[self.name] = value141        142        # Track changes143        if not hasattr(obj, '_changes'):144            obj._changes = []145        146        obj._changes.append({147            'field': self.name,148            'old': old_value,149            'new': value150        })151152class TrackedModel:153    name = Tracked()154    value = Tracked()
    output
    Tracked attributes:
  42. self.name ← None

    pass 1 of 2
    126class Tracked:127    def __init__(self⟨Tracked P⟩):128        self.name→ None = None
  43. name ← (empty)

    152class TrackedModel:153    name→ (empty) = Tracked()154    value = Tracked()
  44. self.name ← None

    pass 2 of 2
    126class Tracked:127    def __init__(self⟨Tracked Q⟩):128        self.name→ None = None
  45. value ← (empty)

    153name = Tracked()154value→ (empty) = Tracked()
  46. self.name ← name

    pass 1 of 2
    130def __set_name__(self⟨Tracked P⟩, owner<class '__main__.TrackedModel'>, namename):131    self.name→ name = namename
  47. self.name ← value

    pass 2 of 2
    130def __set_name__(self⟨Tracked Q⟩, owner<class '__main__.TrackedModel'>, namevalue):131    self.name→ value = namevalue
  48. model ← ⟨TrackedModel R⟩

    156model→ ⟨TrackedModel R⟩ = TrackedModel()157model.name = "Initial"158model.value = 100
  49. old_value ← None, obj.__dict__[self.name] ← Initial

    pass 1 of 4
    138def __set__(self⟨Tracked P⟩, obj⟨TrackedModel R⟩, valueInitial):139    old_value→ None = obj.__dict__{}.get(self.namename)140    obj.__dict__[self.name]→ Initial = valueInitial
    All 4 passes — pass 1 is the card above
    passselfvalueobj.__dict__self.nameold_valueobj.__dict__[self.name]obj._changes
    1⟨Tracked P⟩Initial{}nameNoneInitial[]
    2⟨Tracked Q⟩100{'name': 'Initial', '_changes': [{'field': 'name', 'old': None, 'new': 'Initial'}]}valueNone100[{'field': 'name', 'old': None, 'new': 'Initial'}] [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}]
    3⟨Tracked P⟩Updated{'name': 'Initial', '_changes': [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}], 'value': 100}nameInitialUpdated[{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}] [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}]
    4⟨Tracked Q⟩200{'name': 'Updated', '_changes': [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}], 'value': 100}value100200[{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}] [{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}, {'field': 'value', 'old': 100, 'new': 200}]
  50. obj._changes ← []

    142# Track changes143if not hasattr(obj⟨TrackedModel R⟩, '_changes'):144    obj._changes→ [] = []
  51. obj._changes ← [{'field': 'name', 'old': None, 'new': 'Initial'}]

    146obj._changes→ [{'field': 'name', 'old': None, 'new': 'Initial'}].append({147    'field': self.namename,148    'old': old_valueNone,149    'new': valueInitial150})
  52. model.name ← Initial

    156model = TrackedModel()157model.name→ Initial = "Initial"158model.value = 100159model.name = "Updated"
  53. model.value ← 100

    157model.name = "Initial"158model.value→ 100 = 100159model.name = "Updated"160model.value = 200
  54. model.name ← Updated

    158model.value = 100159model.name→ Updated = "Updated"160model.value = 200
  55. model.value ← 200

    159model.name = "Updated"160model.value→ 200 = 200161162print("Changes:")163for change in model._changes:
    outputChanges:
  56. for change in model._changes:

    pass 1 of 4
    162print("Changes:")163for change{'field': 'name', 'old': None, 'new': 'Initial'} in model._changes[{'field': 'name', 'old': None, 'new': 'Initial'}, {'field': 'value', 'old': None, 'new': 100}, {'field': 'name', 'old': 'Initial', 'new': 'Updated'}, {'field': 'value', 'old': 100, 'new': 200}]:164    print(f"  {change['field']name}: {change['old']None} -> {change['new']Initial}")
    output  name: None -> Initial
    All 4 passes — pass 1 is the card above
    passchangechange[’field’]change[’old’]change[’new’]
    1{'field': 'name', 'old': None, 'new': 'Initial'}nameNoneInitial
    2{'field': 'value', 'old': None, 'new': 100}valueNone100
    3{'field': 'name', 'old': 'Initial', 'new': 'Updated'}nameInitialUpdated
    4{'field': 'value', 'old': 100, 'new': 200}value100200
  57. print(" Type conversion:")

    166# Type conversion167print("\nType conversion:")168169class Converted:170    def __init__(self, converter):171        self.converter = converter172        self.name = None173    174    def __set_name__(self, owner, name):175        self.name = name176    177    def __get__(self, obj, type=None):178        if obj is None:179            return self180        return obj.__dict__.get(self.name)181    182    def __set__(self, obj, value):183        converted = self.converter(value)184        obj.__dict__[self.name] = converted185186class Config:187    port = Converted(int)188    debug = Converted(bool)
    output
    Type conversion:
  58. self.converter ← <class 'int'>, self.name ← None

    pass 1 of 3
    169class Converted:170    def __init__(self⟨Converted S⟩, converter<class 'int'>):171        self.converter→ <class 'int'> = converter<class 'int'>172        self.name→ None = None
    All 3 passes — pass 1 is the card above
    passselfconverterself.converterself.name
    1⟨Converted S⟩<class 'int'><class 'int'>None
    2⟨Converted T⟩<class 'bool'><class 'bool'>None
    3⟨Converted U⟩<class 'float'><class 'float'>None
  59. port ← (empty)

    186class Config:187    port→ (empty) = Converted(int)188    debug = Converted(bool)189    timeout = Converted(float)
  60. debug ← (empty)

    187port = Converted(int)188debug→ (empty) = Converted(bool)189timeout = Converted(float)
  61. timeout ← (empty)

    188debug = Converted(bool)189timeout→ (empty) = Converted(float)
  62. self.name ← port

    pass 1 of 3
    174def __set_name__(self⟨Converted S⟩, owner<class '__main__.Config'>, nameport):175    self.name→ port = nameport
    All 3 passes — pass 1 is the card above
    passselfnameself.name
    1⟨Converted S⟩portport
    2⟨Converted T⟩debugdebug
    3⟨Converted U⟩timeouttimeout
  63. config ← ⟨Config V⟩

    191config→ ⟨Config V⟩ = Config()192config.port = "8080"193config.debug = "yes"
  64. converted ← 8080, obj.__dict__[self.name] ← 8080

    pass 1 of 3
    182def __set__(self⟨Converted S⟩, obj⟨Config V⟩, value8080):183    converted→ 8080 = self.converter(value8080)184    obj.__dict__[self.name]→ 8080 = converted8080
    All 3 passes — pass 1 is the card above
    passselfvalueconvertedobj.__dict__[self.name]
    1⟨Converted S⟩808080808080
    2⟨Converted T⟩yesTrueTrue
    3⟨Converted U⟩30.530.530.5
  65. config.port ← 8080

    191config = Config()192config.port→ 8080 = "8080"193config.debug = "yes"194config.timeout = "30.5"
  66. config.debug ← True

    192config.port = "8080"193config.debug→ True = "yes"194config.timeout = "30.5"
  67. config.timeout ← 30.5

    193config.debug = "yes"194config.timeout→ 30.5 = "30.5"195196print(f"Config: port={config.port8080} ({type(config.port).__name__})")197print(f"        debug={config.debug} ({type(config.debug).__name__})")
  68. def __get__(self, obj, type=None):

    pass 1 of 6
    177def __get__(self⟨Converted S⟩, obj⟨Config V⟩, type<class '__main__.Config'>=NoneNone):178    if obj is None:179        return self180    return obj.__dict__{'port': 8080, 'debug': True, 'timeout': 30.5}.get(self.nameport)
    All 6 passes — pass 1 is the card above
    passselfself.name
    1⟨Converted S⟩port
    2⟨Converted S⟩port
    3⟨Converted T⟩debug
    4⟨Converted T⟩debug
    5⟨Converted U⟩timeout
    6⟨Converted U⟩timeout
  69. print(f"Config: port={config.port} ({type(config.port).__name__})")

    196print(f"Config: port={config.port8080} ({type(config.port).__name__})")197print(f"        debug={config.debugTrue} ({type(config.debug).__name__})")198print(f"        timeout={config.timeout} ({type(config.timeout).__name__})")
    outputConfig: port=8080 (int)
  70. print(f" debug={config.debug} ({type(config.debug).__name__})")

    196print(f"Config: port={config.port} ({type(config.port).__name__})")197print(f"        debug={config.debugTrue} ({type(config.debug).__name__})")198print(f"        timeout={config.timeout30.5} ({type(config.timeout).__name__})")
    output        debug=True (bool)
  71. print(f" timeout={config.timeout} ({type(config.timeout).__name…

    197print(f"        debug={config.debug} ({type(config.debug).__name__})")198print(f"        timeout={config.timeout30.5} ({type(config.timeout).__name__})")199200# Units descriptor201print("\nUnits descriptor:")202203class Quantity:204    def __init__(self, unit):205        self.unit = unit206        self.name = None207    208    def __set_name__(self, owner, name):209        self.name = name210    211    def __get__(self, obj, type=None):212        if obj is None:213            return self214        value = obj.__dict__.get(self.name, 0)215        return f"{value} {self.unit}"216    217    def __set__(self, obj, value):218        if not isinstance(value, (int, float)):219            raise TypeError(f"{self.name} must be numeric")220        obj.__dict__[self.name] = value221222class Product:223    weight = Quantity("kg")224    length = Quantity("cm")
    output        timeout=30.5 (float)
    
    Units descriptor:
  72. self.unit ← kg, self.name ← None

    pass 1 of 3
    203class Quantity:204    def __init__(self⟨Quantity W⟩, unitkg):205        self.unit→ kg = unitkg206        self.name→ None = None
    All 3 passes — pass 1 is the card above
    passselfunitself.unitself.name
    1⟨Quantity W⟩kgkgNone
    2⟨Quantity X⟩cmcmNone
    3⟨Quantity Y⟩USDUSDNone
  73. weight ← (empty)

    222class Product:223    weight→ (empty) = Quantity("kg")224    length = Quantity("cm")225    price = Quantity("USD")
  74. length ← (empty)

    223weight = Quantity("kg")224length→ (empty) = Quantity("cm")225price = Quantity("USD")
  75. price ← (empty)

    224length = Quantity("cm")225price→ (empty) = Quantity("USD")
  76. self.name ← weight

    pass 1 of 3
    208def __set_name__(self⟨Quantity W⟩, owner<class '__main__.Product'>, nameweight):209    self.name→ weight = nameweight
    All 3 passes — pass 1 is the card above
    passselfnameself.name
    1⟨Quantity W⟩weightweight
    2⟨Quantity X⟩lengthlength
    3⟨Quantity Y⟩priceprice
  77. product ← ⟨Product Z⟩

    227product→ ⟨Product Z⟩ = Product()228product.weight = 2.5229product.length = 30
  78. obj.__dict__[self.name] ← 2.5

    pass 1 of 3
    217def __set__(self⟨Quantity W⟩, obj⟨Product Z⟩, value2.5):218    if not isinstance(value, (int, float)):219        raise TypeError(f"{self.name} must be numeric")220    obj.__dict__[self.name]→ 2.5 = value2.5
    All 3 passes — pass 1 is the card above
    passselfvalueobj.__dict__[self.name]
    1⟨Quantity W⟩2.52.5
    2⟨Quantity X⟩3030
    3⟨Quantity Y⟩19.9919.99
  79. product.weight ← 2.5 kg

    227product = Product()228product.weight→ 2.5 kg = 2.5229product.length = 30230product.price = 19.99
  80. product.length ← 30 cm

    228product.weight = 2.5229product.length→ 30 cm = 30230product.price = 19.99
  81. product.price ← 19.99 USD

    229product.length = 30230product.price→ 19.99 USD = 19.99231232print(f"Product: weight={product.weight2.5 kg}, length={product.length30 cm}, price={product.price19.99 USD}")
  82. value ← 2.5

    pass 1 of 3
    211def __get__(self⟨Quantity W⟩, obj⟨Product Z⟩, type<class '__main__.Product'>=NoneNone):212    if obj is None:213        return self214    value→ 2.5 = obj.__dict__{'weight': 2.5, 'length': 30, 'price': 19.99}.get(self.nameweight, 0)215    return f"{value2.5} {self.unitkg}"
    All 3 passes — pass 1 is the card above
    passselfself.nameself.unitvalue
    1⟨Quantity W⟩weightkg2.5
    2⟨Quantity X⟩lengthcm30
    3⟨Quantity Y⟩priceUSD19.99
  83. print(f"Product: weight={product.weight}, length={product.length}, pri…

    232print(f"Product: weight={product.weight2.5 kg}, length={product.length30 cm}, price={product.price19.99 USD}")233234# Practical example235print("\nPractical example:")236237# Complete model with multiple descriptor types238class IntField:239    def __init__(self, min_value=None, max_value=None):240        self.min_value = min_value241        self.max_value = max_value242        self.name = None243    244    def __set_name__(self, owner, name):245        self.name = name246    247    def __get__(self, obj, type=None):248        if obj is None:249            return self250        return obj.__dict__.get(self.name)251    252    def __set__(self, obj, value):253        if not isinstance(value, int):254            raise TypeError(f"{self.name} must be int")255        if self.min_value is not None and value < self.min_value:256            raise ValueError(f"{self.name} must be >= {self.min_value}")257        if self.max_value is not None and value > self.max_value:258            raise ValueError(f"{self.name} must be <= {self.max_value}")259        obj.__dict__[self.name] = value260261class StringField:262    def __init__(self, max_length=None):263        self.max_length = max_length264        self.name = None265    266    def __set_name__(self, owner, name):267        self.name = name268    269    def __get__(self, obj, type=None):270        if obj is None:271            return self272        return obj.__dict__.get(self.name, "")273    274    def __set__(self, obj, value):275        if not isinstance(value, str):276            raise TypeError(f"{self.name} must be str")277        if self.max_length and len(value) > self.max_length:278            raise ValueError(f"{self.name} too long (max {self.max_length})")279        obj.__dict__[self.name] = value280281class Student:282    id = IntField(min_value=1)283    name = StringField(max_length=50)
    outputProduct: weight=2.5 kg, length=30 cm, price=19.99 USD
    
    Practical example:
  84. self.min_value ← 1, self.max_value ← None, self.name ← None

    pass 1 of 3
    238class IntField:239    def __init__(self⟨IntField AA⟩, min_value1=NoneNone, max_valueNone=None):240        self.min_value→ 1 = min_value1241        self.max_value→ None = max_valueNone242        self.name→ None = None
    All 3 passes — pass 1 is the card above
    passselfmin_valuemax_valueself.min_valueself.max_valueself.name
    1⟨IntField AA⟩1None1NoneNone
    2⟨IntField AB⟩01500150None
    3⟨IntField AC⟩01000100None
  85. id ← <built-in function id>

    281class Student:282    id→ <built-in function id> = IntField(min_value=1)283    name = StringField(max_length=50)284    age = IntField(min_value=0, max_value=150)
  86. self.max_length ← 50, self.name ← None

    261class StringField:262    def __init__(self⟨StringField AD⟩, max_length50=NoneNone):263        self.max_length→ 50 = max_length50264        self.name→ None = None
  87. name ← (empty)

    282id = IntField(min_value=1)283name→ (empty) = StringField(max_length=50)284age = IntField(min_value=0, max_value=150)285grade = IntField(min_value=0, max_value=100)
  88. age ← (empty)

    283name = StringField(max_length=50)284age→ (empty) = IntField(min_value=0, max_value=150)285grade = IntField(min_value=0, max_value=100)
  89. grade ← (empty)

    284age = IntField(min_value=0, max_value=150)285grade→ (empty) = IntField(min_value=0, max_value=100)
  90. self.name ← id

    pass 1 of 3
    244def __set_name__(self⟨IntField AA⟩, owner<class '__main__.Student'>, nameid):245    self.name→ id = nameid
    All 3 passes — pass 1 is the card above
    passselfnameself.name
    1⟨IntField AA⟩idid
    2⟨IntField AB⟩ageage
    3⟨IntField AC⟩gradegrade
  91. self.name ← name

    266def __set_name__(self⟨StringField AD⟩, owner<class '__main__.Student'>, namename):267    self.name→ name = namename
  92. student ← ⟨Student AE⟩

    287student→ ⟨Student AE⟩ = Student()288student.id = 1289student.name = "Alice"
  93. obj.__dict__[self.name] ← 1

    pass 1 of 4
    252def __set__(self⟨IntField AA⟩, obj⟨Student AE⟩, value1):253    if not isinstance(value, int):254        raise TypeError(f"{self.name} must be int")255    if self.min_value is not None and value < self.min_value:256        raise ValueError(f"{self.name} must be >= {self.min_value}")257    if self.max_value is not None and value > self.max_value:258        raise ValueError(f"{self.name} must be <= {self.max_value}")259    obj.__dict__[self.name]→ 1 = value1
    All 4 passes — pass 1 is the card above
    passselfvalueself.max_valueself.nameeobj.__dict__[self.name]
    1⟨IntField AA⟩11
    2⟨IntField AB⟩2020
    3⟨IntField AC⟩8585
    4⟨IntField AC⟩105100gradegrade must be <= 100
  94. student.id ← 1

    287student = Student()288student.id→ 1 = 1289student.name = "Alice"290student.age = 20
  95. obj.__dict__[self.name] ← Alice

    274def __set__(self⟨StringField AD⟩, obj⟨Student AE⟩, valueAlice):275    if not isinstance(value, str):276        raise TypeError(f"{self.name} must be str")277    if self.max_length and len(value) > self.max_length:278        raise ValueError(f"{self.name} too long (max {self.max_length})")279    obj.__dict__[self.name]→ Alice = valueAlice
  96. student.name ← Alice

    288student.id = 1289student.name→ Alice = "Alice"290student.age = 20291student.grade = 85
  97. student.age ← 20

    289student.name = "Alice"290student.age→ 20 = 20291student.grade = 85
  98. student.grade ← 85

    290student.age = 20291student.grade→ 85 = 85292293print(f"Student: id={student.id1}, name={student.nameAlice}, age={student.age20}, grade={student.grade85}")
  99. def __get__(self, obj, type=None):

    pass 1 of 3
    247def __get__(self⟨IntField AA⟩, obj⟨Student AE⟩, type<class '__main__.Student'>=NoneNone):248    if obj is None:249        return self250    return obj.__dict__{'id': 1, 'name': 'Alice', 'age': 20, 'grade': 85}.get(self.nameid)
    All 3 passes — pass 1 is the card above
    passselfself.name
    1⟨IntField AA⟩id
    2⟨IntField AB⟩age
    3⟨IntField AC⟩grade
  100. def __get__(self, obj, type=None):

    269def __get__(self⟨StringField AD⟩, obj⟨Student AE⟩, type<class '__main__.Student'>=NoneNone):270    if obj is None:271        return self272    return obj.__dict__{'id': 1, 'name': 'Alice', 'age': 20, 'grade': 85}.get(self.namename, "")
  101. print(f"Student: id={student.id}, name={student.name}, age={student.ag…

    293print(f"Student: id={student.id1}, name={student.nameAlice}, age={student.age20}, grade={student.grade85}")
    outputStudent: id=1, name=Alice, age=20, grade=85
  102. if self.max_value is not None and value > self.max_value:

    256    raise ValueError(f"{self.name} must be >= {self.min_value}")257if self.max_value100 is not None and value105 > self.max_value:258    raise ValueError(f"{self.namegrade} must be <= {self.max_value100}")259obj.__dict__[self.name] = value
  103. except ValueError as e:

    297    student.grade = 105298except ValueError as e:299    print(f"Error: {egrade must be <= 100}")
    outputError: grade must be <= 100

Modern Python frameworks like Django and SQLAlchemy use descriptors extensively for their ORM field definitions.

descriptor applications Real-world uses including ORM fields, lazy loading, validation, change tracking, and type conversion.

Types

  • Data descriptors: Implement __get__() and __set__() (and optionally __delete__())
  • Non-data descriptors: Only implement __get__()

Use Cases

  • Validation and type checking
  • Lazy loading of expensive resources
  • Computed and cached attributes
  • ORM field definitions
  • Logging and debugging attribute access

Exercise: descriptor_practice.py

Create a RangeChecked descriptor that validates numeric attributes are within a specified min/max range