[{"data":1,"prerenderedAt":2705},["ShallowReactive",2],{"page-\u002Fpython\u002F15-properties-and-descriptors":3},{"id":4,"title":5,"body":6,"description":27,"extension":2699,"meta":2700,"navigation":88,"path":2701,"seo":2702,"stem":2703,"__hash__":2704},"content\u002Fpython\u002F15-properties-and-descriptors.md","15 — Properties & Descriptors",{"type":7,"value":8,"toc":2676},"minimark",[9,13,18,225,246,252,503,521,528,705,720,727,917,952,1012,1028,1032,1056,1381,1399,1404,1442,1516,1522,1531,1708,1714,1720,1898,1907,1916,2100,2113,2117,2172,2176,2259,2263,2266,2459,2608,2612,2672],[10,11,5],"h1",{"id":12},"_15-properties-descriptors",[14,15,17],"h2",{"id":16},"why-properties-exist-the-gettersetter-problem","Why Properties Exist: The Getter\u002FSetter Problem",[19,20,22],"code-wrapper",{"language":21},"python",[23,24,28],"pre",{"className":25,"code":26,"language":21,"meta":27,"style":27},"language-python shiki shiki-themes github-light github-dark","# The naive Java\u002FC#-style approach — verbose, and Python discourages it\nclass Temperature:\n    def __init__(self, celsius):\n        self._celsius = celsius\n\n    def get_celsius(self):\n        return self._celsius\n\n    def set_celsius(self, value):\n        if value \u003C -273.15:\n            raise ValueError(\"Below absolute zero\")\n        self._celsius = value\n\nt = Temperature(25)\nt.set_celsius(30)          # verbose call syntax, unlike plain attribute access\nprint(t.get_celsius())\n","",[29,30,31,40,55,68,83,90,101,113,118,129,149,168,180,185,201,216],"code",{"__ignoreMap":27},[32,33,36],"span",{"class":34,"line":35},"line",1,[32,37,39],{"class":38},"sdCPZ","# The naive Java\u002FC#-style approach — verbose, and Python discourages it\n",[32,41,43,47,51],{"class":34,"line":42},2,[32,44,46],{"class":45},"svdQ7","class",[32,48,50],{"class":49},"sIsaT"," Temperature",[32,52,54],{"class":53},"ssxIu",":\n",[32,56,58,61,65],{"class":34,"line":57},3,[32,59,60],{"class":45},"    def",[32,62,64],{"class":63},"snvgF"," __init__",[32,66,67],{"class":53},"(self, celsius):\n",[32,69,71,74,77,80],{"class":34,"line":70},4,[32,72,73],{"class":63},"        self",[32,75,76],{"class":53},"._celsius ",[32,78,79],{"class":45},"=",[32,81,82],{"class":53}," celsius\n",[32,84,86],{"class":34,"line":85},5,[32,87,89],{"emptyLinePlaceholder":88},true,"\n",[32,91,93,95,98],{"class":34,"line":92},6,[32,94,60],{"class":45},[32,96,97],{"class":49}," get_celsius",[32,99,100],{"class":53},"(self):\n",[32,102,104,107,110],{"class":34,"line":103},7,[32,105,106],{"class":45},"        return",[32,108,109],{"class":63}," self",[32,111,112],{"class":53},"._celsius\n",[32,114,116],{"class":34,"line":115},8,[32,117,89],{"emptyLinePlaceholder":88},[32,119,121,123,126],{"class":34,"line":120},9,[32,122,60],{"class":45},[32,124,125],{"class":49}," set_celsius",[32,127,128],{"class":53},"(self, value):\n",[32,130,132,135,138,141,144,147],{"class":34,"line":131},10,[32,133,134],{"class":45},"        if",[32,136,137],{"class":53}," value ",[32,139,140],{"class":45},"\u003C",[32,142,143],{"class":45}," -",[32,145,146],{"class":63},"273.15",[32,148,54],{"class":53},[32,150,152,155,158,161,165],{"class":34,"line":151},11,[32,153,154],{"class":45},"            raise",[32,156,157],{"class":63}," ValueError",[32,159,160],{"class":53},"(",[32,162,164],{"class":163},"sJ6F3","\"Below absolute zero\"",[32,166,167],{"class":53},")\n",[32,169,171,173,175,177],{"class":34,"line":170},12,[32,172,73],{"class":63},[32,174,76],{"class":53},[32,176,79],{"class":45},[32,178,179],{"class":53}," value\n",[32,181,183],{"class":34,"line":182},13,[32,184,89],{"emptyLinePlaceholder":88},[32,186,188,191,193,196,199],{"class":34,"line":187},14,[32,189,190],{"class":53},"t ",[32,192,79],{"class":45},[32,194,195],{"class":53}," Temperature(",[32,197,198],{"class":63},"25",[32,200,167],{"class":53},[32,202,204,207,210,213],{"class":34,"line":203},15,[32,205,206],{"class":53},"t.set_celsius(",[32,208,209],{"class":63},"30",[32,211,212],{"class":53},")          ",[32,214,215],{"class":38},"# verbose call syntax, unlike plain attribute access\n",[32,217,219,222],{"class":34,"line":218},16,[32,220,221],{"class":63},"print",[32,223,224],{"class":53},"(t.get_celsius())\n",[226,227,228,229,233,234,237,238,241,242,245],"p",{},"Python's idiom is different: start with a ",[230,231,232],"strong",{},"plain public attribute",", and only introduce a ",[29,235,236],{},"@property"," later if you need validation, computed values, or side effects — crucially, this upgrade doesn't break any existing caller's ",[29,239,240],{},"obj.attr"," \u002F ",[29,243,244],{},"obj.attr = value"," syntax.",[14,247,249,251],{"id":248},"property-computed-attributes-that-look-like-data",[29,250,236],{}," — Computed Attributes That Look Like Data",[19,253,254],{"language":21},[23,255,257],{"className":25,"code":256,"language":21,"meta":27,"style":27},"class Temperature:\n    def __init__(self, celsius):\n        self._celsius = celsius       # convention: leading underscore for the \"backing\" attribute\n\n    @property\n    def celsius(self):\n        return self._celsius\n\n    @celsius.setter\n    def celsius(self, value):\n        if value \u003C -273.15:\n            raise ValueError(\"Below absolute zero\")\n        self._celsius = value\n\n    @property\n    def fahrenheit(self):             # a fully computed, read-only property — no backing field at all\n        return self._celsius * 9 \u002F 5 + 32\n\nt = Temperature(25)\nprint(t.celsius)         # 25 — looks like plain attribute access, but calls the getter\nprint(t.fahrenheit)        # 77.0\nt.celsius = 30              # looks like plain assignment, but calls the setter (validates!)\nprint(t.celsius)               # 30\n\n# t.celsius = -300         # ValueError: Below absolute zero\n# t.fahrenheit = 100        # AttributeError: can't set attribute (no setter defined)\n",[29,258,259,267,275,289,293,301,310,318,322,327,335,349,361,371,375,381,394,421,426,439,450,461,475,486,491,497],{"__ignoreMap":27},[32,260,261,263,265],{"class":34,"line":35},[32,262,46],{"class":45},[32,264,50],{"class":49},[32,266,54],{"class":53},[32,268,269,271,273],{"class":34,"line":42},[32,270,60],{"class":45},[32,272,64],{"class":63},[32,274,67],{"class":53},[32,276,277,279,281,283,286],{"class":34,"line":57},[32,278,73],{"class":63},[32,280,76],{"class":53},[32,282,79],{"class":45},[32,284,285],{"class":53}," celsius       ",[32,287,288],{"class":38},"# convention: leading underscore for the \"backing\" attribute\n",[32,290,291],{"class":34,"line":70},[32,292,89],{"emptyLinePlaceholder":88},[32,294,295,298],{"class":34,"line":85},[32,296,297],{"class":49},"    @",[32,299,300],{"class":63},"property\n",[32,302,303,305,308],{"class":34,"line":92},[32,304,60],{"class":45},[32,306,307],{"class":49}," celsius",[32,309,100],{"class":53},[32,311,312,314,316],{"class":34,"line":103},[32,313,106],{"class":45},[32,315,109],{"class":63},[32,317,112],{"class":53},[32,319,320],{"class":34,"line":115},[32,321,89],{"emptyLinePlaceholder":88},[32,323,324],{"class":34,"line":120},[32,325,326],{"class":49},"    @celsius.setter\n",[32,328,329,331,333],{"class":34,"line":131},[32,330,60],{"class":45},[32,332,307],{"class":49},[32,334,128],{"class":53},[32,336,337,339,341,343,345,347],{"class":34,"line":151},[32,338,134],{"class":45},[32,340,137],{"class":53},[32,342,140],{"class":45},[32,344,143],{"class":45},[32,346,146],{"class":63},[32,348,54],{"class":53},[32,350,351,353,355,357,359],{"class":34,"line":170},[32,352,154],{"class":45},[32,354,157],{"class":63},[32,356,160],{"class":53},[32,358,164],{"class":163},[32,360,167],{"class":53},[32,362,363,365,367,369],{"class":34,"line":182},[32,364,73],{"class":63},[32,366,76],{"class":53},[32,368,79],{"class":45},[32,370,179],{"class":53},[32,372,373],{"class":34,"line":187},[32,374,89],{"emptyLinePlaceholder":88},[32,376,377,379],{"class":34,"line":203},[32,378,297],{"class":49},[32,380,300],{"class":63},[32,382,383,385,388,391],{"class":34,"line":218},[32,384,60],{"class":45},[32,386,387],{"class":49}," fahrenheit",[32,389,390],{"class":53},"(self):             ",[32,392,393],{"class":38},"# a fully computed, read-only property — no backing field at all\n",[32,395,397,399,401,403,406,409,412,415,418],{"class":34,"line":396},17,[32,398,106],{"class":45},[32,400,109],{"class":63},[32,402,76],{"class":53},[32,404,405],{"class":45},"*",[32,407,408],{"class":63}," 9",[32,410,411],{"class":45}," \u002F",[32,413,414],{"class":63}," 5",[32,416,417],{"class":45}," +",[32,419,420],{"class":63}," 32\n",[32,422,424],{"class":34,"line":423},18,[32,425,89],{"emptyLinePlaceholder":88},[32,427,429,431,433,435,437],{"class":34,"line":428},19,[32,430,190],{"class":53},[32,432,79],{"class":45},[32,434,195],{"class":53},[32,436,198],{"class":63},[32,438,167],{"class":53},[32,440,442,444,447],{"class":34,"line":441},20,[32,443,221],{"class":63},[32,445,446],{"class":53},"(t.celsius)         ",[32,448,449],{"class":38},"# 25 — looks like plain attribute access, but calls the getter\n",[32,451,453,455,458],{"class":34,"line":452},21,[32,454,221],{"class":63},[32,456,457],{"class":53},"(t.fahrenheit)        ",[32,459,460],{"class":38},"# 77.0\n",[32,462,464,467,469,472],{"class":34,"line":463},22,[32,465,466],{"class":53},"t.celsius ",[32,468,79],{"class":45},[32,470,471],{"class":63}," 30",[32,473,474],{"class":38},"              # looks like plain assignment, but calls the setter (validates!)\n",[32,476,478,480,483],{"class":34,"line":477},23,[32,479,221],{"class":63},[32,481,482],{"class":53},"(t.celsius)               ",[32,484,485],{"class":38},"# 30\n",[32,487,489],{"class":34,"line":488},24,[32,490,89],{"emptyLinePlaceholder":88},[32,492,494],{"class":34,"line":493},25,[32,495,496],{"class":38},"# t.celsius = -300         # ValueError: Below absolute zero\n",[32,498,500],{"class":34,"line":499},26,[32,501,502],{"class":38},"# t.fahrenheit = 100        # AttributeError: can't set attribute (no setter defined)\n",[226,504,505,506,509,510,512,513,516,517,520],{},"Callers can't tell from the call site whether ",[29,507,508],{},"t.celsius"," is a plain attribute or a ",[29,511,236],{}," — this is the point: you can start with a plain attribute and refactor to a property later without changing any calling code, unlike languages where you must commit to ",[29,514,515],{},"getX()","\u002F",[29,518,519],{},"setX()"," method syntax from day one if you might ever need validation.",[14,522,524,525],{"id":523},"read-only-properties-and-xdeleter","Read-Only Properties and ",[29,526,527],{},"@x.deleter",[19,529,530],{"language":21},[23,531,533],{"className":25,"code":532,"language":21,"meta":27,"style":27},"class Account:\n    def __init__(self, balance):\n        self._balance = balance\n        self._closed = False\n\n    @property\n    def balance(self):\n        return self._balance\n\n    @balance.deleter\n    def balance(self):\n        if self._balance != 0:\n            raise RuntimeError(\"Cannot close account with non-zero balance\")\n        self._closed = True\n        print(\"Account closed\")\n\nacc = Account(0)\ndel acc.balance   # Account closed — calls the deleter, NOT actual attribute deletion semantics\n",[29,534,535,544,553,565,577,581,587,596,605,609,614,622,638,652,663,675,679,694],{"__ignoreMap":27},[32,536,537,539,542],{"class":34,"line":35},[32,538,46],{"class":45},[32,540,541],{"class":49}," Account",[32,543,54],{"class":53},[32,545,546,548,550],{"class":34,"line":42},[32,547,60],{"class":45},[32,549,64],{"class":63},[32,551,552],{"class":53},"(self, balance):\n",[32,554,555,557,560,562],{"class":34,"line":57},[32,556,73],{"class":63},[32,558,559],{"class":53},"._balance ",[32,561,79],{"class":45},[32,563,564],{"class":53}," balance\n",[32,566,567,569,572,574],{"class":34,"line":70},[32,568,73],{"class":63},[32,570,571],{"class":53},"._closed ",[32,573,79],{"class":45},[32,575,576],{"class":63}," False\n",[32,578,579],{"class":34,"line":85},[32,580,89],{"emptyLinePlaceholder":88},[32,582,583,585],{"class":34,"line":92},[32,584,297],{"class":49},[32,586,300],{"class":63},[32,588,589,591,594],{"class":34,"line":103},[32,590,60],{"class":45},[32,592,593],{"class":49}," balance",[32,595,100],{"class":53},[32,597,598,600,602],{"class":34,"line":115},[32,599,106],{"class":45},[32,601,109],{"class":63},[32,603,604],{"class":53},"._balance\n",[32,606,607],{"class":34,"line":120},[32,608,89],{"emptyLinePlaceholder":88},[32,610,611],{"class":34,"line":131},[32,612,613],{"class":49},"    @balance.deleter\n",[32,615,616,618,620],{"class":34,"line":151},[32,617,60],{"class":45},[32,619,593],{"class":49},[32,621,100],{"class":53},[32,623,624,626,628,630,633,636],{"class":34,"line":170},[32,625,134],{"class":45},[32,627,109],{"class":63},[32,629,559],{"class":53},[32,631,632],{"class":45},"!=",[32,634,635],{"class":63}," 0",[32,637,54],{"class":53},[32,639,640,642,645,647,650],{"class":34,"line":182},[32,641,154],{"class":45},[32,643,644],{"class":63}," RuntimeError",[32,646,160],{"class":53},[32,648,649],{"class":163},"\"Cannot close account with non-zero balance\"",[32,651,167],{"class":53},[32,653,654,656,658,660],{"class":34,"line":187},[32,655,73],{"class":63},[32,657,571],{"class":53},[32,659,79],{"class":45},[32,661,662],{"class":63}," True\n",[32,664,665,668,670,673],{"class":34,"line":203},[32,666,667],{"class":63},"        print",[32,669,160],{"class":53},[32,671,672],{"class":163},"\"Account closed\"",[32,674,167],{"class":53},[32,676,677],{"class":34,"line":218},[32,678,89],{"emptyLinePlaceholder":88},[32,680,681,684,686,689,692],{"class":34,"line":396},[32,682,683],{"class":53},"acc ",[32,685,79],{"class":45},[32,687,688],{"class":53}," Account(",[32,690,691],{"class":63},"0",[32,693,167],{"class":53},[32,695,696,699,702],{"class":34,"line":423},[32,697,698],{"class":45},"del",[32,700,701],{"class":53}," acc.balance   ",[32,703,704],{"class":38},"# Account closed — calls the deleter, NOT actual attribute deletion semantics\n",[226,706,707,708,711,712,715,716,719],{},"A property with only a getter (no ",[29,709,710],{},"@x.setter",") is effectively read-only from the outside — attempting ",[29,713,714],{},"obj.x = value"," raises ",[29,717,718],{},"AttributeError: can't set attribute",", which is a clean, explicit way to expose computed or immutable-after-construction values.",[14,721,723,726],{"id":722},"functoolscached_property-compute-once-cache-on-the-instance",[29,724,725],{},"functools.cached_property"," — Compute Once, Cache on the Instance",[19,728,729],{"language":21},[23,730,732],{"className":25,"code":731,"language":21,"meta":27,"style":27},"from functools import cached_property\nimport time\n\nclass Report:\n    def __init__(self, rows):\n        self.rows = rows\n\n    @cached_property\n    def total(self):\n        print(\"Computing total...\")\n        time.sleep(1)                 # simulate expensive computation\n        return sum(row[\"amount\"] for row in self.rows)\n\nr = Report([{\"amount\": 10}, {\"amount\": 20}])\nprint(r.total)   # \"Computing total...\" then 30 — computed once\nprint(r.total)     # 30 — instantly, from cache, NO recomputation, no \"Computing total...\" printed\n",[29,733,734,748,755,759,768,777,789,793,798,807,818,832,862,866,897,907],{"__ignoreMap":27},[32,735,736,739,742,745],{"class":34,"line":35},[32,737,738],{"class":45},"from",[32,740,741],{"class":53}," functools ",[32,743,744],{"class":45},"import",[32,746,747],{"class":53}," cached_property\n",[32,749,750,752],{"class":34,"line":42},[32,751,744],{"class":45},[32,753,754],{"class":53}," time\n",[32,756,757],{"class":34,"line":57},[32,758,89],{"emptyLinePlaceholder":88},[32,760,761,763,766],{"class":34,"line":70},[32,762,46],{"class":45},[32,764,765],{"class":49}," Report",[32,767,54],{"class":53},[32,769,770,772,774],{"class":34,"line":85},[32,771,60],{"class":45},[32,773,64],{"class":63},[32,775,776],{"class":53},"(self, rows):\n",[32,778,779,781,784,786],{"class":34,"line":92},[32,780,73],{"class":63},[32,782,783],{"class":53},".rows ",[32,785,79],{"class":45},[32,787,788],{"class":53}," rows\n",[32,790,791],{"class":34,"line":103},[32,792,89],{"emptyLinePlaceholder":88},[32,794,795],{"class":34,"line":115},[32,796,797],{"class":49},"    @cached_property\n",[32,799,800,802,805],{"class":34,"line":120},[32,801,60],{"class":45},[32,803,804],{"class":49}," total",[32,806,100],{"class":53},[32,808,809,811,813,816],{"class":34,"line":131},[32,810,667],{"class":63},[32,812,160],{"class":53},[32,814,815],{"class":163},"\"Computing total...\"",[32,817,167],{"class":53},[32,819,820,823,826,829],{"class":34,"line":151},[32,821,822],{"class":53},"        time.sleep(",[32,824,825],{"class":63},"1",[32,827,828],{"class":53},")                 ",[32,830,831],{"class":38},"# simulate expensive computation\n",[32,833,834,836,839,842,845,848,851,854,857,859],{"class":34,"line":170},[32,835,106],{"class":45},[32,837,838],{"class":63}," sum",[32,840,841],{"class":53},"(row[",[32,843,844],{"class":163},"\"amount\"",[32,846,847],{"class":53},"] ",[32,849,850],{"class":45},"for",[32,852,853],{"class":53}," row ",[32,855,856],{"class":45},"in",[32,858,109],{"class":63},[32,860,861],{"class":53},".rows)\n",[32,863,864],{"class":34,"line":182},[32,865,89],{"emptyLinePlaceholder":88},[32,867,868,871,873,876,878,881,884,887,889,891,894],{"class":34,"line":187},[32,869,870],{"class":53},"r ",[32,872,79],{"class":45},[32,874,875],{"class":53}," Report([{",[32,877,844],{"class":163},[32,879,880],{"class":53},": ",[32,882,883],{"class":63},"10",[32,885,886],{"class":53},"}, {",[32,888,844],{"class":163},[32,890,880],{"class":53},[32,892,893],{"class":63},"20",[32,895,896],{"class":53},"}])\n",[32,898,899,901,904],{"class":34,"line":203},[32,900,221],{"class":63},[32,902,903],{"class":53},"(r.total)   ",[32,905,906],{"class":38},"# \"Computing total...\" then 30 — computed once\n",[32,908,909,911,914],{"class":34,"line":218},[32,910,221],{"class":63},[32,912,913],{"class":53},"(r.total)     ",[32,915,916],{"class":38},"# 30 — instantly, from cache, NO recomputation, no \"Computing total...\" printed\n",[226,918,919,920,922,923,926,927,933,934,936,937,939,940,943,944,947,948,951],{},"Unlike ",[29,921,236],{},", ",[29,924,925],{},"cached_property"," ",[230,928,929,930],{},"stores the result directly in the instance's ",[29,931,932],{},"__dict__"," after the first access, under the same name — subsequent lookups find it there before ever reaching the descriptor, permanently short-circuiting recomputation. This means ",[29,935,925],{}," requires the instance to have a ",[29,938,932],{}," (incompatible with ",[29,941,942],{},"__slots__"," unless you add ",[29,945,946],{},"\"__dict__\""," to the slots, which partially defeats the purpose) and means the cached value goes stale if the underlying data (",[29,949,950],{},"self.rows",") changes after first access.",[19,953,954],{"language":21},[23,955,957],{"className":25,"code":956,"language":21,"meta":27,"style":27},"r2 = Report([{\"amount\": 5}])\nprint(r2.total)   # 5\nr2.rows.append({\"amount\": 100})\nprint(r2.total)     # STILL 5 — stale! cached_property doesn't know self.rows changed\n",[29,958,959,977,987,1002],{"__ignoreMap":27},[32,960,961,964,966,968,970,972,975],{"class":34,"line":35},[32,962,963],{"class":53},"r2 ",[32,965,79],{"class":45},[32,967,875],{"class":53},[32,969,844],{"class":163},[32,971,880],{"class":53},[32,973,974],{"class":63},"5",[32,976,896],{"class":53},[32,978,979,981,984],{"class":34,"line":42},[32,980,221],{"class":63},[32,982,983],{"class":53},"(r2.total)   ",[32,985,986],{"class":38},"# 5\n",[32,988,989,992,994,996,999],{"class":34,"line":57},[32,990,991],{"class":53},"r2.rows.append({",[32,993,844],{"class":163},[32,995,880],{"class":53},[32,997,998],{"class":63},"100",[32,1000,1001],{"class":53},"})\n",[32,1003,1004,1006,1009],{"class":34,"line":70},[32,1005,221],{"class":63},[32,1007,1008],{"class":53},"(r2.total)     ",[32,1010,1011],{"class":38},"# STILL 5 — stale! cached_property doesn't know self.rows changed\n",[226,1013,1014,1017,1018,1020,1021,1024,1025,1027],{},[230,1015,1016],{},"Best practice",": only use ",[29,1019,925],{}," for values derived from data that's genuinely immutable for the object's lifetime — otherwise, invalidate manually (",[29,1022,1023],{},"del instance.__dict__[\"total\"]",") or use plain ",[29,1026,236],{}," if the underlying data can change.",[14,1029,1031],{"id":1030},"the-descriptor-protocol","The Descriptor Protocol",[226,1033,1034,1036,1037,1040,1041,1044,1045,516,1048,1051,1052,1055],{},[29,1035,236],{}," is itself implemented using the ",[230,1038,1039],{},"descriptor protocol"," — any class implementing ",[29,1042,1043],{},"__get__"," (and optionally ",[29,1046,1047],{},"__set__",[29,1049,1050],{},"__delete__",") becomes a descriptor, and Python's attribute-lookup machinery treats descriptor attributes specially when they're stored on a ",[1053,1054,46],"em",{},".",[19,1057,1058],{"language":21},[23,1059,1061],{"className":25,"code":1060,"language":21,"meta":27,"style":27},"class PositiveNumber:\n    \"\"\"A reusable descriptor enforcing 'must be positive' on ANY attribute it's assigned to.\"\"\"\n\n    def __set_name__(self, owner, name):\n        self.name = f\"_{name}\"                   # remembers the attribute name it's attached as\n\n    def __get__(self, instance, owner):\n        if instance is None:                       # accessed on the CLASS, not an instance\n            return self\n        return getattr(instance, self.name)\n\n    def __set__(self, instance, value):\n        if value \u003C= 0:\n            raise ValueError(f\"{self.name[1:]} must be positive, got {value}\")\n        setattr(instance, self.name, value)\n\nclass Product:\n    price = PositiveNumber()          # descriptor instance, shared at the CLASS level\n    quantity = PositiveNumber()         # a second, independent descriptor instance\n\n    def __init__(self, price, quantity):\n        self.price = price                # triggers PositiveNumber.__set__\n        self.quantity = quantity\n\np = Product(9.99, 3)\nprint(p.price, p.quantity)   # 9.99 3\n\n# p.price = -5   # ValueError: price must be positive, got -5\n",[29,1062,1063,1072,1077,1081,1091,1121,1125,1135,1154,1162,1178,1182,1192,1205,1245,1257,1261,1270,1283,1296,1300,1309,1324,1336,1340,1360,1370,1375],{"__ignoreMap":27},[32,1064,1065,1067,1070],{"class":34,"line":35},[32,1066,46],{"class":45},[32,1068,1069],{"class":49}," PositiveNumber",[32,1071,54],{"class":53},[32,1073,1074],{"class":34,"line":42},[32,1075,1076],{"class":163},"    \"\"\"A reusable descriptor enforcing 'must be positive' on ANY attribute it's assigned to.\"\"\"\n",[32,1078,1079],{"class":34,"line":57},[32,1080,89],{"emptyLinePlaceholder":88},[32,1082,1083,1085,1088],{"class":34,"line":70},[32,1084,60],{"class":45},[32,1086,1087],{"class":63}," __set_name__",[32,1089,1090],{"class":53},"(self, owner, name):\n",[32,1092,1093,1095,1098,1100,1103,1106,1109,1112,1115,1118],{"class":34,"line":85},[32,1094,73],{"class":63},[32,1096,1097],{"class":53},".name ",[32,1099,79],{"class":45},[32,1101,1102],{"class":45}," f",[32,1104,1105],{"class":163},"\"_",[32,1107,1108],{"class":63},"{",[32,1110,1111],{"class":53},"name",[32,1113,1114],{"class":63},"}",[32,1116,1117],{"class":163},"\"",[32,1119,1120],{"class":38},"                   # remembers the attribute name it's attached as\n",[32,1122,1123],{"class":34,"line":92},[32,1124,89],{"emptyLinePlaceholder":88},[32,1126,1127,1129,1132],{"class":34,"line":103},[32,1128,60],{"class":45},[32,1130,1131],{"class":63}," __get__",[32,1133,1134],{"class":53},"(self, instance, owner):\n",[32,1136,1137,1139,1142,1145,1148,1151],{"class":34,"line":115},[32,1138,134],{"class":45},[32,1140,1141],{"class":53}," instance ",[32,1143,1144],{"class":45},"is",[32,1146,1147],{"class":63}," None",[32,1149,1150],{"class":53},":                       ",[32,1152,1153],{"class":38},"# accessed on the CLASS, not an instance\n",[32,1155,1156,1159],{"class":34,"line":120},[32,1157,1158],{"class":45},"            return",[32,1160,1161],{"class":63}," self\n",[32,1163,1164,1166,1169,1172,1175],{"class":34,"line":131},[32,1165,106],{"class":45},[32,1167,1168],{"class":63}," getattr",[32,1170,1171],{"class":53},"(instance, ",[32,1173,1174],{"class":63},"self",[32,1176,1177],{"class":53},".name)\n",[32,1179,1180],{"class":34,"line":151},[32,1181,89],{"emptyLinePlaceholder":88},[32,1183,1184,1186,1189],{"class":34,"line":170},[32,1185,60],{"class":45},[32,1187,1188],{"class":63}," __set__",[32,1190,1191],{"class":53},"(self, instance, value):\n",[32,1193,1194,1196,1198,1201,1203],{"class":34,"line":182},[32,1195,134],{"class":45},[32,1197,137],{"class":53},[32,1199,1200],{"class":45},"\u003C=",[32,1202,635],{"class":63},[32,1204,54],{"class":53},[32,1206,1207,1209,1211,1213,1216,1218,1221,1224,1226,1229,1231,1234,1236,1239,1241,1243],{"class":34,"line":187},[32,1208,154],{"class":45},[32,1210,157],{"class":63},[32,1212,160],{"class":53},[32,1214,1215],{"class":45},"f",[32,1217,1117],{"class":163},[32,1219,1220],{"class":63},"{self",[32,1222,1223],{"class":53},".name[",[32,1225,825],{"class":63},[32,1227,1228],{"class":53},":]",[32,1230,1114],{"class":63},[32,1232,1233],{"class":163}," must be positive, got ",[32,1235,1108],{"class":63},[32,1237,1238],{"class":53},"value",[32,1240,1114],{"class":63},[32,1242,1117],{"class":163},[32,1244,167],{"class":53},[32,1246,1247,1250,1252,1254],{"class":34,"line":203},[32,1248,1249],{"class":63},"        setattr",[32,1251,1171],{"class":53},[32,1253,1174],{"class":63},[32,1255,1256],{"class":53},".name, value)\n",[32,1258,1259],{"class":34,"line":218},[32,1260,89],{"emptyLinePlaceholder":88},[32,1262,1263,1265,1268],{"class":34,"line":396},[32,1264,46],{"class":45},[32,1266,1267],{"class":49}," Product",[32,1269,54],{"class":53},[32,1271,1272,1275,1277,1280],{"class":34,"line":423},[32,1273,1274],{"class":53},"    price ",[32,1276,79],{"class":45},[32,1278,1279],{"class":53}," PositiveNumber()          ",[32,1281,1282],{"class":38},"# descriptor instance, shared at the CLASS level\n",[32,1284,1285,1288,1290,1293],{"class":34,"line":428},[32,1286,1287],{"class":53},"    quantity ",[32,1289,79],{"class":45},[32,1291,1292],{"class":53}," PositiveNumber()         ",[32,1294,1295],{"class":38},"# a second, independent descriptor instance\n",[32,1297,1298],{"class":34,"line":441},[32,1299,89],{"emptyLinePlaceholder":88},[32,1301,1302,1304,1306],{"class":34,"line":452},[32,1303,60],{"class":45},[32,1305,64],{"class":63},[32,1307,1308],{"class":53},"(self, price, quantity):\n",[32,1310,1311,1313,1316,1318,1321],{"class":34,"line":463},[32,1312,73],{"class":63},[32,1314,1315],{"class":53},".price ",[32,1317,79],{"class":45},[32,1319,1320],{"class":53}," price                ",[32,1322,1323],{"class":38},"# triggers PositiveNumber.__set__\n",[32,1325,1326,1328,1331,1333],{"class":34,"line":477},[32,1327,73],{"class":63},[32,1329,1330],{"class":53},".quantity ",[32,1332,79],{"class":45},[32,1334,1335],{"class":53}," quantity\n",[32,1337,1338],{"class":34,"line":488},[32,1339,89],{"emptyLinePlaceholder":88},[32,1341,1342,1345,1347,1350,1353,1355,1358],{"class":34,"line":493},[32,1343,1344],{"class":53},"p ",[32,1346,79],{"class":45},[32,1348,1349],{"class":53}," Product(",[32,1351,1352],{"class":63},"9.99",[32,1354,922],{"class":53},[32,1356,1357],{"class":63},"3",[32,1359,167],{"class":53},[32,1361,1362,1364,1367],{"class":34,"line":499},[32,1363,221],{"class":63},[32,1365,1366],{"class":53},"(p.price, p.quantity)   ",[32,1368,1369],{"class":38},"# 9.99 3\n",[32,1371,1373],{"class":34,"line":1372},27,[32,1374,89],{"emptyLinePlaceholder":88},[32,1376,1378],{"class":34,"line":1377},28,[32,1379,1380],{"class":38},"# p.price = -5   # ValueError: price must be positive, got -5\n",[226,1382,1383,1384,1387,1388,1391,1392,1395,1396,1398],{},"One ",[29,1385,1386],{},"PositiveNumber"," instance is written once and reused for both ",[29,1389,1390],{},"price"," and ",[29,1393,1394],{},"quantity"," (and any other class that needs the same validation) — this is the descriptor protocol's real payoff: validation\u002Fcomputation logic factored out of the class body entirely, instead of copy-pasted into multiple near-identical ",[29,1397,236],{}," getters\u002Fsetters.",[1400,1401,1403],"h3",{"id":1402},"data-descriptors-vs-non-data-descriptors","Data descriptors vs non-data descriptors",[226,1405,1406,1407,1409,1410,1412,1413,1416,1417,1419,1420,1412,1422,1425,1426,1428,1429,1431,1432,1434,1435,1438,1439,1441],{},"A descriptor defining ",[29,1408,1047],{}," or ",[29,1411,1050],{}," is a ",[230,1414,1415],{},"data descriptor"," and takes priority over instance ",[29,1418,932],{}," entries; one defining only ",[29,1421,1043],{},[230,1423,1424],{},"non-data descriptor"," and instance ",[29,1427,932],{}," takes priority over it. This is precisely why ",[29,1430,236],{}," (a data descriptor — it defines ",[29,1433,1047],{}," even for \"read-only\" properties, to raise ",[29,1436,1437],{},"AttributeError"," properly) always wins over an instance attribute of the same name, while plain functions (non-data descriptors, via ",[29,1440,1043],{}," for bound-method creation) can be shadowed by an instance attribute of the same name.",[19,1443,1444],{"language":21},[23,1445,1447],{"className":25,"code":1446,"language":21,"meta":27,"style":27},"class Example:\n    def method(self):\n        return \"class method\"\n\ne = Example()\ne.method = lambda: \"instance override\"    # shadows the function descriptor — functions are non-data\nprint(e.method())   # \"instance override\" — instance __dict__ wins over a non-data descriptor\n",[29,1448,1449,1458,1467,1474,1478,1488,1506],{"__ignoreMap":27},[32,1450,1451,1453,1456],{"class":34,"line":35},[32,1452,46],{"class":45},[32,1454,1455],{"class":49}," Example",[32,1457,54],{"class":53},[32,1459,1460,1462,1465],{"class":34,"line":42},[32,1461,60],{"class":45},[32,1463,1464],{"class":49}," method",[32,1466,100],{"class":53},[32,1468,1469,1471],{"class":34,"line":57},[32,1470,106],{"class":45},[32,1472,1473],{"class":163}," \"class method\"\n",[32,1475,1476],{"class":34,"line":70},[32,1477,89],{"emptyLinePlaceholder":88},[32,1479,1480,1483,1485],{"class":34,"line":85},[32,1481,1482],{"class":53},"e ",[32,1484,79],{"class":45},[32,1486,1487],{"class":53}," Example()\n",[32,1489,1490,1493,1495,1498,1500,1503],{"class":34,"line":92},[32,1491,1492],{"class":53},"e.method ",[32,1494,79],{"class":45},[32,1496,1497],{"class":45}," lambda",[32,1499,880],{"class":53},[32,1501,1502],{"class":163},"\"instance override\"",[32,1504,1505],{"class":38},"    # shadows the function descriptor — functions are non-data\n",[32,1507,1508,1510,1513],{"class":34,"line":103},[32,1509,221],{"class":63},[32,1511,1512],{"class":53},"(e.method())   ",[32,1514,1515],{"class":38},"# \"instance override\" — instance __dict__ wins over a non-data descriptor\n",[14,1517,1519,1521],{"id":1518},"__slots__-trading-flexibility-for-memory",[29,1520,942],{}," — Trading Flexibility for Memory",[226,1523,1524,1525,1527,1528,1530],{},"By default, every instance carries a ",[29,1526,932],{}," to hold its attributes — flexible, but with real per-instance memory overhead. ",[29,1529,942],{}," declares a fixed set of allowed attribute names, storing them in a more compact fixed-layout structure instead.",[19,1532,1533],{"language":21},[23,1534,1536],{"className":25,"code":1535,"language":21,"meta":27,"style":27},"class PointDict:\n    def __init__(self, x, y):\n        self.x, self.y = x, y\n\nclass PointSlots:\n    __slots__ = (\"x\", \"y\")           # ONLY x and y are allowed — no __dict__ at all\n    def __init__(self, x, y):\n        self.x, self.y = x, y\n\nimport sys\npd, ps = PointDict(1, 2), PointSlots(1, 2)\nprint(sys.getsizeof(pd.__dict__))   # typically 64+ bytes just for the dict itself\n# print(ps.__dict__)                # AttributeError: 'PointSlots' object has no attribute '__dict__'\n\nps.z = 5     # AttributeError: 'PointSlots' object has no attribute 'z'\n",[29,1537,1538,1547,1556,1573,1577,1586,1611,1619,1633,1637,1644,1672,1687,1692,1696],{"__ignoreMap":27},[32,1539,1540,1542,1545],{"class":34,"line":35},[32,1541,46],{"class":45},[32,1543,1544],{"class":49}," PointDict",[32,1546,54],{"class":53},[32,1548,1549,1551,1553],{"class":34,"line":42},[32,1550,60],{"class":45},[32,1552,64],{"class":63},[32,1554,1555],{"class":53},"(self, x, y):\n",[32,1557,1558,1560,1563,1565,1568,1570],{"class":34,"line":57},[32,1559,73],{"class":63},[32,1561,1562],{"class":53},".x, ",[32,1564,1174],{"class":63},[32,1566,1567],{"class":53},".y ",[32,1569,79],{"class":45},[32,1571,1572],{"class":53}," x, y\n",[32,1574,1575],{"class":34,"line":70},[32,1576,89],{"emptyLinePlaceholder":88},[32,1578,1579,1581,1584],{"class":34,"line":85},[32,1580,46],{"class":45},[32,1582,1583],{"class":49}," PointSlots",[32,1585,54],{"class":53},[32,1587,1588,1591,1594,1597,1600,1602,1605,1608],{"class":34,"line":92},[32,1589,1590],{"class":63},"    __slots__",[32,1592,1593],{"class":45}," =",[32,1595,1596],{"class":53}," (",[32,1598,1599],{"class":163},"\"x\"",[32,1601,922],{"class":53},[32,1603,1604],{"class":163},"\"y\"",[32,1606,1607],{"class":53},")           ",[32,1609,1610],{"class":38},"# ONLY x and y are allowed — no __dict__ at all\n",[32,1612,1613,1615,1617],{"class":34,"line":103},[32,1614,60],{"class":45},[32,1616,64],{"class":63},[32,1618,1555],{"class":53},[32,1620,1621,1623,1625,1627,1629,1631],{"class":34,"line":115},[32,1622,73],{"class":63},[32,1624,1562],{"class":53},[32,1626,1174],{"class":63},[32,1628,1567],{"class":53},[32,1630,79],{"class":45},[32,1632,1572],{"class":53},[32,1634,1635],{"class":34,"line":120},[32,1636,89],{"emptyLinePlaceholder":88},[32,1638,1639,1641],{"class":34,"line":131},[32,1640,744],{"class":45},[32,1642,1643],{"class":53}," sys\n",[32,1645,1646,1649,1651,1654,1656,1658,1661,1664,1666,1668,1670],{"class":34,"line":151},[32,1647,1648],{"class":53},"pd, ps ",[32,1650,79],{"class":45},[32,1652,1653],{"class":53}," PointDict(",[32,1655,825],{"class":63},[32,1657,922],{"class":53},[32,1659,1660],{"class":63},"2",[32,1662,1663],{"class":53},"), PointSlots(",[32,1665,825],{"class":63},[32,1667,922],{"class":53},[32,1669,1660],{"class":63},[32,1671,167],{"class":53},[32,1673,1674,1676,1679,1681,1684],{"class":34,"line":170},[32,1675,221],{"class":63},[32,1677,1678],{"class":53},"(sys.getsizeof(pd.",[32,1680,932],{"class":63},[32,1682,1683],{"class":53},"))   ",[32,1685,1686],{"class":38},"# typically 64+ bytes just for the dict itself\n",[32,1688,1689],{"class":34,"line":182},[32,1690,1691],{"class":38},"# print(ps.__dict__)                # AttributeError: 'PointSlots' object has no attribute '__dict__'\n",[32,1693,1694],{"class":34,"line":187},[32,1695,89],{"emptyLinePlaceholder":88},[32,1697,1698,1701,1703,1705],{"class":34,"line":203},[32,1699,1700],{"class":53},"ps.z ",[32,1702,79],{"class":45},[32,1704,414],{"class":63},[32,1706,1707],{"class":38},"     # AttributeError: 'PointSlots' object has no attribute 'z'\n",[226,1709,1710,1711,1713],{},"For classes instantiated in large numbers (rows of parsed data, graph nodes, particles in a simulation), ",[29,1712,942],{}," can cut per-instance memory substantially and slightly speeds up attribute access, since there's no dict hashing involved — the tradeoff is losing the ability to add arbitrary attributes at runtime, and added complexity around inheritance.",[1400,1715,1717,1719],{"id":1716},"__slots__-and-inheritance-gotchas",[29,1718,942],{}," and inheritance gotchas",[19,1721,1722],{"language":21},[23,1723,1725],{"className":25,"code":1724,"language":21,"meta":27,"style":27},"class Base:\n    __slots__ = (\"a\",)\n\nclass Derived(Base):\n    __slots__ = (\"b\",)      # each class in the hierarchy declares its OWN slots\n\nd = Derived()\nd.a = 1    # OK — inherited slot\nd.b = 2      # OK — own slot\nprint(d.a, d.b)   # 1 2\n\nclass DerivedNoSlots(Base):\n    pass                    # forgetting __slots__ here silently RE-ADDS a __dict__!\n\ndns = DerivedNoSlots()\ndns.a = 1\ndns.anything_at_all = \"oops\"   # works! the memory-saving benefit is gone for this subclass\n",[29,1726,1727,1736,1750,1754,1769,1786,1790,1800,1813,1826,1836,1840,1853,1861,1865,1875,1885],{"__ignoreMap":27},[32,1728,1729,1731,1734],{"class":34,"line":35},[32,1730,46],{"class":45},[32,1732,1733],{"class":49}," Base",[32,1735,54],{"class":53},[32,1737,1738,1740,1742,1744,1747],{"class":34,"line":42},[32,1739,1590],{"class":63},[32,1741,1593],{"class":45},[32,1743,1596],{"class":53},[32,1745,1746],{"class":163},"\"a\"",[32,1748,1749],{"class":53},",)\n",[32,1751,1752],{"class":34,"line":57},[32,1753,89],{"emptyLinePlaceholder":88},[32,1755,1756,1758,1761,1763,1766],{"class":34,"line":70},[32,1757,46],{"class":45},[32,1759,1760],{"class":49}," Derived",[32,1762,160],{"class":53},[32,1764,1765],{"class":49},"Base",[32,1767,1768],{"class":53},"):\n",[32,1770,1771,1773,1775,1777,1780,1783],{"class":34,"line":85},[32,1772,1590],{"class":63},[32,1774,1593],{"class":45},[32,1776,1596],{"class":53},[32,1778,1779],{"class":163},"\"b\"",[32,1781,1782],{"class":53},",)      ",[32,1784,1785],{"class":38},"# each class in the hierarchy declares its OWN slots\n",[32,1787,1788],{"class":34,"line":92},[32,1789,89],{"emptyLinePlaceholder":88},[32,1791,1792,1795,1797],{"class":34,"line":103},[32,1793,1794],{"class":53},"d ",[32,1796,79],{"class":45},[32,1798,1799],{"class":53}," Derived()\n",[32,1801,1802,1805,1807,1810],{"class":34,"line":115},[32,1803,1804],{"class":53},"d.a ",[32,1806,79],{"class":45},[32,1808,1809],{"class":63}," 1",[32,1811,1812],{"class":38},"    # OK — inherited slot\n",[32,1814,1815,1818,1820,1823],{"class":34,"line":120},[32,1816,1817],{"class":53},"d.b ",[32,1819,79],{"class":45},[32,1821,1822],{"class":63}," 2",[32,1824,1825],{"class":38},"      # OK — own slot\n",[32,1827,1828,1830,1833],{"class":34,"line":131},[32,1829,221],{"class":63},[32,1831,1832],{"class":53},"(d.a, d.b)   ",[32,1834,1835],{"class":38},"# 1 2\n",[32,1837,1838],{"class":34,"line":151},[32,1839,89],{"emptyLinePlaceholder":88},[32,1841,1842,1844,1847,1849,1851],{"class":34,"line":170},[32,1843,46],{"class":45},[32,1845,1846],{"class":49}," DerivedNoSlots",[32,1848,160],{"class":53},[32,1850,1765],{"class":49},[32,1852,1768],{"class":53},[32,1854,1855,1858],{"class":34,"line":182},[32,1856,1857],{"class":45},"    pass",[32,1859,1860],{"class":38},"                    # forgetting __slots__ here silently RE-ADDS a __dict__!\n",[32,1862,1863],{"class":34,"line":187},[32,1864,89],{"emptyLinePlaceholder":88},[32,1866,1867,1870,1872],{"class":34,"line":203},[32,1868,1869],{"class":53},"dns ",[32,1871,79],{"class":45},[32,1873,1874],{"class":53}," DerivedNoSlots()\n",[32,1876,1877,1880,1882],{"class":34,"line":218},[32,1878,1879],{"class":53},"dns.a ",[32,1881,79],{"class":45},[32,1883,1884],{"class":63}," 1\n",[32,1886,1887,1890,1892,1895],{"class":34,"line":396},[32,1888,1889],{"class":53},"dns.anything_at_all ",[32,1891,79],{"class":45},[32,1893,1894],{"class":163}," \"oops\"",[32,1896,1897],{"class":38},"   # works! the memory-saving benefit is gone for this subclass\n",[226,1899,1900,1901,1903,1904,1906],{},"If even one class in an inheritance chain omits ",[29,1902,942],{},", that subclass (and everything below it) regains a ",[29,1905,932],{},", silently defeating the memory optimization for the entire branch — a mistake that's easy to make and easy to miss in code review, since nothing errors.",[14,1908,1910,1911,1913,1914],{"id":1909},"combining-property-with-__slots__","Combining ",[29,1912,236],{}," with ",[29,1915,942],{},[19,1917,1918],{"language":21},[23,1919,1921],{"className":25,"code":1920,"language":21,"meta":27,"style":27},"class Circle:\n    __slots__ = (\"_radius\",)      # note: the property name \"radius\" is NOT itself a slot\n\n    def __init__(self, radius):\n        self._radius = radius\n\n    @property\n    def radius(self):\n        return self._radius\n\n    @radius.setter\n    def radius(self, value):\n        if value \u003C 0:\n            raise ValueError(\"radius cannot be negative\")\n        self._radius = value\n\nc = Circle(5)\nprint(c.radius)   # 5\nc.radius = 10\nprint(c.radius)     # 10\n",[29,1922,1923,1932,1948,1952,1961,1973,1977,1983,1992,2001,2005,2010,2018,2030,2043,2053,2057,2071,2080,2090],{"__ignoreMap":27},[32,1924,1925,1927,1930],{"class":34,"line":35},[32,1926,46],{"class":45},[32,1928,1929],{"class":49}," Circle",[32,1931,54],{"class":53},[32,1933,1934,1936,1938,1940,1943,1945],{"class":34,"line":42},[32,1935,1590],{"class":63},[32,1937,1593],{"class":45},[32,1939,1596],{"class":53},[32,1941,1942],{"class":163},"\"_radius\"",[32,1944,1782],{"class":53},[32,1946,1947],{"class":38},"# note: the property name \"radius\" is NOT itself a slot\n",[32,1949,1950],{"class":34,"line":57},[32,1951,89],{"emptyLinePlaceholder":88},[32,1953,1954,1956,1958],{"class":34,"line":70},[32,1955,60],{"class":45},[32,1957,64],{"class":63},[32,1959,1960],{"class":53},"(self, radius):\n",[32,1962,1963,1965,1968,1970],{"class":34,"line":85},[32,1964,73],{"class":63},[32,1966,1967],{"class":53},"._radius ",[32,1969,79],{"class":45},[32,1971,1972],{"class":53}," radius\n",[32,1974,1975],{"class":34,"line":92},[32,1976,89],{"emptyLinePlaceholder":88},[32,1978,1979,1981],{"class":34,"line":103},[32,1980,297],{"class":49},[32,1982,300],{"class":63},[32,1984,1985,1987,1990],{"class":34,"line":115},[32,1986,60],{"class":45},[32,1988,1989],{"class":49}," radius",[32,1991,100],{"class":53},[32,1993,1994,1996,1998],{"class":34,"line":120},[32,1995,106],{"class":45},[32,1997,109],{"class":63},[32,1999,2000],{"class":53},"._radius\n",[32,2002,2003],{"class":34,"line":131},[32,2004,89],{"emptyLinePlaceholder":88},[32,2006,2007],{"class":34,"line":151},[32,2008,2009],{"class":49},"    @radius.setter\n",[32,2011,2012,2014,2016],{"class":34,"line":170},[32,2013,60],{"class":45},[32,2015,1989],{"class":49},[32,2017,128],{"class":53},[32,2019,2020,2022,2024,2026,2028],{"class":34,"line":182},[32,2021,134],{"class":45},[32,2023,137],{"class":53},[32,2025,140],{"class":45},[32,2027,635],{"class":63},[32,2029,54],{"class":53},[32,2031,2032,2034,2036,2038,2041],{"class":34,"line":187},[32,2033,154],{"class":45},[32,2035,157],{"class":63},[32,2037,160],{"class":53},[32,2039,2040],{"class":163},"\"radius cannot be negative\"",[32,2042,167],{"class":53},[32,2044,2045,2047,2049,2051],{"class":34,"line":203},[32,2046,73],{"class":63},[32,2048,1967],{"class":53},[32,2050,79],{"class":45},[32,2052,179],{"class":53},[32,2054,2055],{"class":34,"line":218},[32,2056,89],{"emptyLinePlaceholder":88},[32,2058,2059,2062,2064,2067,2069],{"class":34,"line":396},[32,2060,2061],{"class":53},"c ",[32,2063,79],{"class":45},[32,2065,2066],{"class":53}," Circle(",[32,2068,974],{"class":63},[32,2070,167],{"class":53},[32,2072,2073,2075,2078],{"class":34,"line":423},[32,2074,221],{"class":63},[32,2076,2077],{"class":53},"(c.radius)   ",[32,2079,986],{"class":38},[32,2081,2082,2085,2087],{"class":34,"line":428},[32,2083,2084],{"class":53},"c.radius ",[32,2086,79],{"class":45},[32,2088,2089],{"class":63}," 10\n",[32,2091,2092,2094,2097],{"class":34,"line":441},[32,2093,221],{"class":63},[32,2095,2096],{"class":53},"(c.radius)     ",[32,2098,2099],{"class":38},"# 10\n",[226,2101,2102,2103,2105,2106,2108,2109,2112],{},"The ",[29,2104,236],{}," itself lives on the ",[1053,2107,46],{}," (it's a descriptor, not stored per-instance), so it doesn't need — and must not be listed as — a slot; only the actual backing storage (",[29,2110,2111],{},"_radius",") needs a slot entry.",[14,2114,2116],{"id":2115},"tips-tricks","💡 Tips & Tricks",[2118,2119,2120,2130,2142,2153,2164],"ul",{},[2121,2122,2123,2126,2127,2129],"li",{},[230,2124,2125],{},"Idiom",": never start a new class with hand-written getter\u002Fsetter methods \"just in case\" — start with plain public attributes, and only introduce ",[29,2128,236],{}," when validation or computed behavior is actually needed; this is idiomatic Python, not laziness.",[2121,2131,2132,880,2135,1409,2138,2141],{},[230,2133,2134],{},"Debug",[29,2136,2137],{},"vars(ClassName)",[29,2139,2140],{},"ClassName.__dict__"," shows you every descriptor (including properties) defined directly on a class — useful for spotting exactly which attributes are computed vs plain data.",[2121,2143,2144,880,2147,2149,2150,2152],{},[230,2145,2146],{},"Performance",[29,2148,925],{}," trades memory (the cached value lives in ",[29,2151,932],{}," forever, or until deleted) for CPU — appropriate for expensive, rarely-changing derived values; inappropriate for values that need to reflect frequently-mutating source data.",[2121,2154,2155,880,2157,2160,2161,2163],{},[230,2156,2125],{},[29,2158,2159],{},"__set_name__"," (Python 3.6+) is what lets a descriptor know the attribute name it was assigned to without the class explicitly passing it — this is what makes reusable, generic descriptors (like the ",[29,2162,1386],{}," example) practical instead of requiring a name string to be passed to every instantiation.",[2121,2165,2166,2168,2169,2171],{},[230,2167,2146],{},": for very large numbers of simple data-holding instances (parsed rows, coordinates, cache entries), benchmark ",[29,2170,942],{}," before committing — the memory savings are real but the ergonomic cost (no dynamic attributes, more careful inheritance) isn't always worth it for smaller-scale code.",[14,2173,2175],{"id":2174},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[2118,2177,2178,2186,2201,2214,2239],{},[2121,2179,2180,2185],{},[230,2181,2182,2184],{},[29,2183,925],{}," silently returns a stale value once the underlying data changes, since it's computed exactly once and then stored"," — treat it as appropriate only for genuinely immutable-for-the-object's-lifetime derived data, or manage invalidation manually.",[2121,2187,2188,2197,2198,1055],{},[230,2189,2190,2191,2193,2194,2196],{},"Forgetting ",[29,2192,942],{}," on even one subclass in an inheritance chain re-adds a ",[29,2195,932],{}," to every instance of that subclass, silently defeating the memory optimization"," — this produces no error or warning; the class simply stops saving memory, discoverable only by explicitly checking ",[29,2199,2200],{},"instance.__dict__",[2121,2202,2203,2213],{},[230,2204,2205,2206,2208,2209,2212],{},"A property with only a getter is read-only, and assigning to it raises ",[29,2207,718],{}," — a beginner easily confuses this with ",[29,2210,2211],{},"AttributeError: object has no attribute",","," which is the error for accessing something that doesn't exist at all; the messages look similar but the causes (and fixes) are completely different.",[2121,2215,2216,2228,2229,2232,2233,2235,2236,2238],{},[230,2217,2218,2219,2221,2222,2224,2225,2227],{},"Data descriptors (",[29,2220,236],{},", anything defining ",[29,2223,1047],{},") always take priority over instance ",[29,2226,932],{}," entries, while non-data descriptors (plain functions\u002Fmethods) are shadowed by same-named instance attributes"," — this asymmetry is why you can override a bound method on a specific instance (",[29,2230,2231],{},"instance.method = other_func",") but can never similarly \"shadow\" a ",[29,2234,236],{}," by assigning through ",[29,2237,2200],{}," directly.",[2121,2240,2241,2255,2256,2258],{},[230,2242,2243,2245,2246,2248,2249,2251,2252,2254],{},[29,2244,925],{}," requires the instance to support ",[29,2247,932],{}," — combining it with ",[29,2250,942],{}," requires explicitly adding ",[29,2253,946],{}," to the slots tuple",", which reintroduces a per-instance dict and undermines much of the reason to use ",[29,2257,942],{}," in the first place; the two features are in real tension, not simply compatible.",[14,2260,2262],{"id":2261},"spot-the-bug","🧠 Spot the Bug",[226,2264,2265],{},"A configuration object caches an expensive derived value. After updating the underlying settings, callers keep getting outdated results. Find the bug.",[19,2267,2268],{"language":21},[23,2269,2271],{"className":25,"code":2270,"language":21,"meta":27,"style":27},"from functools import cached_property\n\nclass AppConfig:\n    def __init__(self, settings):\n        self.settings = settings\n\n    @cached_property\n    def connection_string(self):\n        host = self.settings[\"host\"]\n        port = self.settings[\"port\"]\n        return f\"{host}:{port}\"\n\nconfig = AppConfig({\"host\": \"db1.internal\", \"port\": 5432})\nprint(config.connection_string)\n\nconfig.settings[\"host\"] = \"db2.internal\"\nprint(config.connection_string)\n",[29,2272,2273,2283,2287,2296,2305,2317,2321,2325,2334,2352,2368,2396,2400,2428,2435,2439,2453],{"__ignoreMap":27},[32,2274,2275,2277,2279,2281],{"class":34,"line":35},[32,2276,738],{"class":45},[32,2278,741],{"class":53},[32,2280,744],{"class":45},[32,2282,747],{"class":53},[32,2284,2285],{"class":34,"line":42},[32,2286,89],{"emptyLinePlaceholder":88},[32,2288,2289,2291,2294],{"class":34,"line":57},[32,2290,46],{"class":45},[32,2292,2293],{"class":49}," AppConfig",[32,2295,54],{"class":53},[32,2297,2298,2300,2302],{"class":34,"line":70},[32,2299,60],{"class":45},[32,2301,64],{"class":63},[32,2303,2304],{"class":53},"(self, settings):\n",[32,2306,2307,2309,2312,2314],{"class":34,"line":85},[32,2308,73],{"class":63},[32,2310,2311],{"class":53},".settings ",[32,2313,79],{"class":45},[32,2315,2316],{"class":53}," settings\n",[32,2318,2319],{"class":34,"line":92},[32,2320,89],{"emptyLinePlaceholder":88},[32,2322,2323],{"class":34,"line":103},[32,2324,797],{"class":49},[32,2326,2327,2329,2332],{"class":34,"line":115},[32,2328,60],{"class":45},[32,2330,2331],{"class":49}," connection_string",[32,2333,100],{"class":53},[32,2335,2336,2339,2341,2343,2346,2349],{"class":34,"line":120},[32,2337,2338],{"class":53},"        host ",[32,2340,79],{"class":45},[32,2342,109],{"class":63},[32,2344,2345],{"class":53},".settings[",[32,2347,2348],{"class":163},"\"host\"",[32,2350,2351],{"class":53},"]\n",[32,2353,2354,2357,2359,2361,2363,2366],{"class":34,"line":131},[32,2355,2356],{"class":53},"        port ",[32,2358,79],{"class":45},[32,2360,109],{"class":63},[32,2362,2345],{"class":53},[32,2364,2365],{"class":163},"\"port\"",[32,2367,2351],{"class":53},[32,2369,2370,2372,2374,2376,2378,2381,2383,2386,2388,2391,2393],{"class":34,"line":151},[32,2371,106],{"class":45},[32,2373,1102],{"class":45},[32,2375,1117],{"class":163},[32,2377,1108],{"class":63},[32,2379,2380],{"class":53},"host",[32,2382,1114],{"class":63},[32,2384,2385],{"class":163},":",[32,2387,1108],{"class":63},[32,2389,2390],{"class":53},"port",[32,2392,1114],{"class":63},[32,2394,2395],{"class":163},"\"\n",[32,2397,2398],{"class":34,"line":170},[32,2399,89],{"emptyLinePlaceholder":88},[32,2401,2402,2405,2407,2410,2412,2414,2417,2419,2421,2423,2426],{"class":34,"line":182},[32,2403,2404],{"class":53},"config ",[32,2406,79],{"class":45},[32,2408,2409],{"class":53}," AppConfig({",[32,2411,2348],{"class":163},[32,2413,880],{"class":53},[32,2415,2416],{"class":163},"\"db1.internal\"",[32,2418,922],{"class":53},[32,2420,2365],{"class":163},[32,2422,880],{"class":53},[32,2424,2425],{"class":63},"5432",[32,2427,1001],{"class":53},[32,2429,2430,2432],{"class":34,"line":187},[32,2431,221],{"class":63},[32,2433,2434],{"class":53},"(config.connection_string)\n",[32,2436,2437],{"class":34,"line":203},[32,2438,89],{"emptyLinePlaceholder":88},[32,2440,2441,2444,2446,2448,2450],{"class":34,"line":218},[32,2442,2443],{"class":53},"config.settings[",[32,2445,2348],{"class":163},[32,2447,847],{"class":53},[32,2449,79],{"class":45},[32,2451,2452],{"class":163}," \"db2.internal\"\n",[32,2454,2455,2457],{"class":34,"line":396},[32,2456,221],{"class":63},[32,2458,2434],{"class":53},[2460,2461,2462,2466,2497,2507,2593,2600],"details",{},[2463,2464,2465],"summary",{},"Answer",[226,2467,2468,2469,2472,2473,2475,2476,2479,2480,2483,2484,926,2486,2489,2490,2492,2493,2496],{},"Both prints show ",[29,2470,2471],{},"db1.internal:5432"," — the second one is stale. ",[29,2474,925],{}," computes ",[29,2477,2478],{},"connection_string"," on first access and stores the result directly in ",[29,2481,2482],{},"config.__dict__[\"connection_string\"]",". From that point on, attribute lookup finds the cached value in the instance's own ",[29,2485,932],{},[1053,2487,2488],{},"before"," the ",[29,2491,925],{}," descriptor ever runs again — it has no way to know ",[29,2494,2495],{},"self.settings"," changed underneath it, because it never re-executes the getter function at all after the first call.",[226,2498,2499,2500,2503,2504,2506],{},"If the value genuinely needs to reflect live changes to ",[29,2501,2502],{},"settings",", use a plain ",[29,2505,236],{}," instead (recomputes every access, at the cost of doing the work every time):",[19,2508,2509],{"language":21},[23,2510,2512],{"className":25,"code":2511,"language":21,"meta":27,"style":27},"class AppConfig:\n    def __init__(self, settings):\n        self.settings = settings\n\n    @property\n    def connection_string(self):\n        return f\"{self.settings['host']}:{self.settings['port']}\"\n",[29,2513,2514,2522,2530,2540,2544,2550,2558],{"__ignoreMap":27},[32,2515,2516,2518,2520],{"class":34,"line":35},[32,2517,46],{"class":45},[32,2519,2293],{"class":49},[32,2521,54],{"class":53},[32,2523,2524,2526,2528],{"class":34,"line":42},[32,2525,60],{"class":45},[32,2527,64],{"class":63},[32,2529,2304],{"class":53},[32,2531,2532,2534,2536,2538],{"class":34,"line":57},[32,2533,73],{"class":63},[32,2535,2311],{"class":53},[32,2537,79],{"class":45},[32,2539,2316],{"class":53},[32,2541,2542],{"class":34,"line":70},[32,2543,89],{"emptyLinePlaceholder":88},[32,2545,2546,2548],{"class":34,"line":85},[32,2547,297],{"class":49},[32,2549,300],{"class":63},[32,2551,2552,2554,2556],{"class":34,"line":92},[32,2553,60],{"class":45},[32,2555,2331],{"class":49},[32,2557,100],{"class":53},[32,2559,2560,2562,2564,2566,2568,2570,2573,2576,2578,2580,2582,2584,2587,2589,2591],{"class":34,"line":103},[32,2561,106],{"class":45},[32,2563,1102],{"class":45},[32,2565,1117],{"class":163},[32,2567,1220],{"class":63},[32,2569,2345],{"class":53},[32,2571,2572],{"class":163},"'host'",[32,2574,2575],{"class":53},"]",[32,2577,1114],{"class":63},[32,2579,2385],{"class":163},[32,2581,1220],{"class":63},[32,2583,2345],{"class":53},[32,2585,2586],{"class":163},"'port'",[32,2588,2575],{"class":53},[32,2590,1114],{"class":63},[32,2592,2395],{"class":163},[226,2594,2595,2596,2599],{},"Or, if caching is still wanted, invalidate explicitly whenever settings change: ",[29,2597,2598],{},"del config.__dict__[\"connection_string\"]"," (or wrap settings mutation in a method that does this).",[226,2601,2602,880,2605,2607],{},[230,2603,2604],{},"The lesson",[29,2606,925],{}," caches forever by default — it is only safe for values derived from data that doesn't change after the value is first read, not a general-purpose memoization tool for \"usually stable\" data.",[14,2609,2611],{"id":2610},"key-takeaways","Key Takeaways",[2118,2613,2614,2623,2634,2642,2655,2664],{},[2121,2615,2616,2617,2619,2620,2622],{},"Start classes with plain public attributes; upgrade to ",[29,2618,236],{}," later for validation or computed values — the calling syntax (",[29,2621,240],{},") stays identical either way, which is the entire benefit over Java\u002FC#-style getters\u002Fsetters.",[2121,2624,2625,2626,2628,2629,1391,2631,2633],{},"A property with only a getter is read-only (",[29,2627,1437],{}," on assignment); ",[29,2630,710],{},[29,2632,527],{}," add write\u002Fdelete behavior under the same attribute name.",[2121,2635,2636,2638,2639,2641],{},[29,2637,725],{}," computes once and caches the result in the instance's ",[29,2640,932],{}," — fast on repeat access, but silently stale if the underlying source data changes afterward.",[2121,2643,2644,2645,516,2647,516,2649,2651,2652,2654],{},"The descriptor protocol (",[29,2646,1043],{},[29,2648,1047],{},[29,2650,2159],{},") is what ",[29,2653,236],{}," is built on — write a custom descriptor when the same validation\u002Fcomputation logic needs to be reused across multiple attributes or classes.",[2121,2656,2657,2658,2660,2661,2663],{},"Data descriptors (define ",[29,2659,1047],{},") take priority over instance ",[29,2662,932],{},"; non-data descriptors (plain functions) are shadowed by same-named instance attributes — this is why methods can be overridden per-instance but properties can't.",[2121,2665,2666,2668,2669,2671],{},[29,2667,942],{}," trades dynamic-attribute flexibility for lower memory use and slightly faster attribute access — but every class in an inheritance chain must declare it, or a ",[29,2670,932],{}," silently reappears on subclasses that omit it.",[2673,2674,2675],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}",{"title":27,"searchDepth":42,"depth":42,"links":2677},[2678,2679,2681,2683,2685,2688,2693,2695,2696,2697,2698],{"id":16,"depth":42,"text":17},{"id":248,"depth":42,"text":2680},"@property — Computed Attributes That Look Like Data",{"id":523,"depth":42,"text":2682},"Read-Only Properties and @x.deleter",{"id":722,"depth":42,"text":2684},"functools.cached_property — Compute Once, Cache on the Instance",{"id":1030,"depth":42,"text":1031,"children":2686},[2687],{"id":1402,"depth":57,"text":1403},{"id":1518,"depth":42,"text":2689,"children":2690},"__slots__ — Trading Flexibility for Memory",[2691],{"id":1716,"depth":57,"text":2692},"__slots__ and inheritance gotchas",{"id":1909,"depth":42,"text":2694},"Combining @property with __slots__",{"id":2115,"depth":42,"text":2116},{"id":2174,"depth":42,"text":2175},{"id":2261,"depth":42,"text":2262},{"id":2610,"depth":42,"text":2611},"md",{},"\u002Fpython\u002F15-properties-and-descriptors",{"title":5,"description":27},"python\u002F15-properties-and-descriptors","_TrQz4v0oln47vbP7OTOCSwiqYJcCf7AbnZy0NwPx-4",1789924651589]