[{"data":1,"prerenderedAt":603},["ShallowReactive",2],{"post-/posts/an-unexpected-encounter-with-python-class-attributes":3},{"id":4,"title":5,"body":6,"created":594,"description":595,"extension":596,"meta":597,"navigation":83,"path":598,"seo":599,"stem":600,"updated":601,"__hash__":602},"posts/2.posts/20221006.An Unexpected Encounter with Python Class Attributes.md","An Unexpected Encounter with Python Class Attributes",{"type":7,"value":8,"toc":586},"minimark",[9,13,23,26,31,39,127,135,198,201,209,212,219,222,225,229,232,287,290,298,305,311,314,318,324,337,362,380,384,405,414,417,433,440,455,465,468,472,477,545,555,558,582],[10,11,12],"p",{},"Recently, I encountered a mysterious bug in my Python application.\nThe cause of the bug was my poor understanding of:",[14,15,16,20],"ul",{},[17,18,19],"li",{},"how class instances in Python initialise their attributes, and",[17,21,22],{},"how instance attributes and class attributes are resolved.",[10,24,25],{},"In this post I will explain my incorrect assumptions about this part of Python.",[27,28,30],"h2",{"id":29},"people-with-hobbies","People with Hobbies",[10,32,33,34,38],{},"The following Python snippet defines the ",[35,36,37],"code",{},"Person"," class, with a method for adding a hobby to the person's list of hobbies:",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-python shiki shiki-themes one-dark-pro","class Person:\n  hobbies = []\n\n  def add_hobby(self, hobby):\n    self.hobbies.append(hobby)\n","python","",[35,47,48,65,78,85,112],{"__ignoreMap":45},[49,50,53,57,61],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"seHd6","class",[49,58,60],{"class":59},"sU0A5"," Person",[49,62,64],{"class":63},"sn6KH",":\n",[49,66,68,71,75],{"class":51,"line":67},2,[49,69,70],{"class":63},"  hobbies ",[49,72,74],{"class":73},"sjrmR","=",[49,76,77],{"class":63}," []\n",[49,79,81],{"class":51,"line":80},3,[49,82,84],{"emptyLinePlaceholder":83},true,"\n",[49,86,88,91,95,98,102,105,109],{"class":51,"line":87},4,[49,89,90],{"class":55},"  def",[49,92,94],{"class":93},"sVbv2"," add_hobby",[49,96,97],{"class":63},"(",[49,99,101],{"class":100},"sKU4T","self",[49,103,104],{"class":63},", ",[49,106,108],{"class":107},"sb9H8","hobby",[49,110,111],{"class":63},"):\n",[49,113,115,118,121,124],{"class":51,"line":114},5,[49,116,117],{"class":59},"    self",[49,119,120],{"class":63},".hobbies.",[49,122,123],{"class":93},"append",[49,125,126],{"class":63},"(hobby)\n",[10,128,129,130,134],{},"That's marvellous. With this class, we can instantiate some persons (",[131,132,133],"em",{},"people"," in human-speak) and give them hobbies:",[40,136,138],{"className":42,"code":137,"language":44,"meta":45,"style":45},"tolkien = Person()\ntolkien.add_hobby(\"writing\")\n\nelvis = Person()\nelvis.add_hobby(\"music\")\n",[35,139,140,152,169,173,184],{"__ignoreMap":45},[49,141,142,145,147,149],{"class":51,"line":52},[49,143,144],{"class":63},"tolkien ",[49,146,74],{"class":73},[49,148,60],{"class":93},[49,150,151],{"class":63},"()\n",[49,153,154,157,160,162,166],{"class":51,"line":67},[49,155,156],{"class":63},"tolkien.",[49,158,159],{"class":93},"add_hobby",[49,161,97],{"class":63},[49,163,165],{"class":164},"subq3","\"writing\"",[49,167,168],{"class":63},")\n",[49,170,171],{"class":51,"line":80},[49,172,84],{"emptyLinePlaceholder":83},[49,174,175,178,180,182],{"class":51,"line":87},[49,176,177],{"class":63},"elvis ",[49,179,74],{"class":73},[49,181,60],{"class":93},[49,183,151],{"class":63},[49,185,186,189,191,193,196],{"class":51,"line":114},[49,187,188],{"class":63},"elvis.",[49,190,159],{"class":93},[49,192,97],{"class":63},[49,194,195],{"class":164},"\"music\"",[49,197,168],{"class":63},[10,199,200],{},"Now, pause for a moment.",[202,203,204],"blockquote",{},[10,205,206],{},[131,207,208],{},"elevator music starts playing",[10,210,211],{},"Ponder for a while.",[202,213,214],{},[10,215,216],{},[131,217,218],{},"elevator music suddenly stops",[10,220,221],{},"Did you spot the mistake?",[10,223,224],{},"I had been programming in Python for more than five years, and I didn't!",[27,226,228],{"id":227},"unforeseen-consequences","Unforeseen Consequences",[10,230,231],{},"What do you think the following two lines would print?",[40,233,235],{"className":42,"code":234,"language":44,"meta":45,"style":45},"print(f\"Tolkien's hobbies: {tolkien.hobbies}\")\nprint(f\"Elvis's hobbies: {elvis.hobbies}\")\n",[35,236,237,265],{"__ignoreMap":45},[49,238,239,242,244,247,250,254,257,260,263],{"class":51,"line":52},[49,240,241],{"class":73},"print",[49,243,97],{"class":63},[49,245,246],{"class":55},"f",[49,248,249],{"class":164},"\"Tolkien's hobbies: ",[49,251,253],{"class":252},"sVC51","{",[49,255,256],{"class":63},"tolkien.hobbies",[49,258,259],{"class":252},"}",[49,261,262],{"class":164},"\"",[49,264,168],{"class":63},[49,266,267,269,271,273,276,278,281,283,285],{"class":51,"line":67},[49,268,241],{"class":73},[49,270,97],{"class":63},[49,272,246],{"class":55},[49,274,275],{"class":164},"\"Elvis's hobbies: ",[49,277,253],{"class":252},[49,279,280],{"class":63},"elvis.hobbies",[49,282,259],{"class":252},[49,284,262],{"class":164},[49,286,168],{"class":63},[10,288,289],{},"This is what I expected it to print:",[40,291,296],{"className":292,"code":294,"language":295},[293],"language-text","Tolkien's hobbies: ['writing']\nElvis's hobbies: ['music']\n","text",[35,297,294],{"__ignoreMap":45},[10,299,300,301,304],{},"Here's what it ",[131,302,303],{},"actually"," prints:",[40,306,309],{"className":307,"code":308,"language":295},[293],"Tolkien's hobbies: ['writing', 'music']\nElvis's hobbies: ['writing', 'music']\n",[35,310,308],{"__ignoreMap":45},[10,312,313],{},"What?! Why are both hobbies listed for both persons?",[27,315,317],{"id":316},"the-incident","The Incident",[10,319,320,321,323],{},"My bug was rooted in this statement in the ",[35,322,37],{}," class:",[40,325,327],{"className":42,"code":326,"language":44,"meta":45,"style":45},"  hobbies = []\n",[35,328,329],{"__ignoreMap":45},[49,330,331,333,335],{"class":51,"line":52},[49,332,70],{"class":63},[49,334,74],{"class":73},[49,336,77],{"class":63},[10,338,339,340,343,344,346,347,350,351],{},"I ",[131,341,342],{},"thought"," this statement made sure that every new ",[35,345,37],{},"'s ",[35,348,349],{},"hobbies"," attribute was set to a new empty list.",[352,353,354],"sup",{},[355,356,361],"a",{"href":357,"ariaDescribedBy":358,"dataFootnoteRef":45,"id":360},"#user-content-fn-1",[359],"footnote-label","user-content-fnref-1","1",[10,363,364,365,368,369,372,373,376,377,379],{},"However, it actually sets the ",[131,366,367],{},"class attribute"," ",[35,370,371],{},"Person.hobbies"," to an empty list.\nThe statement is only evaluated ",[131,374,375],{},"once",", namely when the class is first evaluated, and no new hobby lists are made when new ",[35,378,37],{}," instances are made.",[27,381,383],{"id":382},"resolving-hobbies","Resolving Hobbies",[10,385,386,387,389,390,393,394,396,397,401,402,404],{},"When appending to the person's hobby list in ",[35,388,159],{},", the attribute ",[35,391,392],{},"self.hobbies"," is used.\nWhen looking up that attribute, Python first looks for a variable named ",[35,395,349],{}," in the instance's namespace.\nIf that fails, ",[398,399,400],"strong",{},"it tries to find the variable in the namespace of the instance's class!","\nSo, it finds the ",[35,403,371],{}," attribute in the class's namespace.",[10,406,407,408,368,410,413],{},"The calls to ",[35,409,159],{},[131,411,412],{},"mutated"," that single list in the class attribute, resulting in all hobbies from different person instances being added to the same list.",[10,415,416],{},"We can verify that they resolve to the same list by evaluating the expression:",[40,418,420],{"className":42,"code":419,"language":44,"meta":45,"style":45},"tolkien.hobbies is elvis.hobbies\n",[35,421,422],{"__ignoreMap":45},[49,423,424,427,430],{"class":51,"line":52},[49,425,426],{"class":63},"tolkien.hobbies ",[49,428,429],{"class":55},"is",[49,431,432],{"class":63}," elvis.hobbies\n",[10,434,435,436,439],{},"which results in ",[35,437,438],{},"True",".",[10,441,442,443,449,450,439],{},"This behaviour is well documented in the ",[355,444,448],{"href":445,"rel":446},"https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables",[447],"nofollow","Python tutorial"," which goes to show that I should have ",[355,451,454],{"href":452,"rel":453},"https://en.wikipedia.org/wiki/RTFM",[447],"RTFM",[10,456,457,460,461,464],{},[131,458,459],{},"Why"," it behaves like this is unclear to me (perhaps I should read some more).\nTo me, it would make sense for Python to raise an ",[35,462,463],{},"AttributeError"," when trying to access an instance attribute that doesn't exist on the instance.",[10,466,467],{},"Feel free to reach out if you can enlighten me.",[27,469,471],{"id":470},"the-fix","The Fix",[10,473,474,475,323],{},"Here's a corrected version of the ",[35,476,37],{},[40,478,480],{"className":42,"code":479,"language":44,"meta":45,"style":45},"class Person:\n  def __init__(self):\n    self.hobbies = []\n\n  def add_hobby(self, hobby):\n    self.hobbies.append(hobby)\n",[35,481,482,490,503,514,518,534],{"__ignoreMap":45},[49,483,484,486,488],{"class":51,"line":52},[49,485,56],{"class":55},[49,487,60],{"class":59},[49,489,64],{"class":63},[49,491,492,494,497,499,501],{"class":51,"line":67},[49,493,90],{"class":55},[49,495,496],{"class":73}," __init__",[49,498,97],{"class":63},[49,500,101],{"class":100},[49,502,111],{"class":63},[49,504,505,507,510,512],{"class":51,"line":80},[49,506,117],{"class":59},[49,508,509],{"class":63},".hobbies ",[49,511,74],{"class":73},[49,513,77],{"class":63},[49,515,516],{"class":51,"line":87},[49,517,84],{"emptyLinePlaceholder":83},[49,519,520,522,524,526,528,530,532],{"class":51,"line":114},[49,521,90],{"class":55},[49,523,94],{"class":93},[49,525,97],{"class":63},[49,527,101],{"class":100},[49,529,104],{"class":63},[49,531,108],{"class":107},[49,533,111],{"class":63},[49,535,537,539,541,543],{"class":51,"line":536},6,[49,538,117],{"class":59},[49,540,120],{"class":63},[49,542,123],{"class":93},[49,544,126],{"class":63},[10,546,547,548,550,551,554],{},"In this version, the ",[35,549,349],{}," attribute is assigned to the instance's namespace in the ",[35,552,553],{},"__init__"," method, which is executed when an instance is initialised.",[10,556,557],{},"This way, every person instance gets its own list of hobbies in its instance attributes.",[559,560,563,568],"section",{"className":561,"dataFootnotes":45},[562],"footnotes",[27,564,567],{"className":565,"id":359},[566],"sr-only","Footnotes",[569,570,571],"ol",{},[17,572,574,575],{"id":573},"user-content-fn-1","This is vaguely how it works in some other programming languages like Java and PHP. ",[355,576,581],{"href":577,"ariaLabel":578,"className":579,"dataFootnoteBackref":45},"#user-content-fnref-1","Back to reference 1",[580],"data-footnote-backref","↩",[583,584,585],"style",{},"html pre.shiki code .seHd6, html code.shiki .seHd6{--shiki-default:#C678DD}html pre.shiki code .sU0A5, html code.shiki .sU0A5{--shiki-default:#E5C07B}html pre.shiki code .sn6KH, html code.shiki .sn6KH{--shiki-default:#ABB2BF}html pre.shiki code .sjrmR, html code.shiki .sjrmR{--shiki-default:#56B6C2}html pre.shiki code .sVbv2, html code.shiki .sVbv2{--shiki-default:#61AFEF}html pre.shiki code .sKU4T, html code.shiki .sKU4T{--shiki-default:#E5C07B;--shiki-default-font-style:italic}html pre.shiki code .sb9H8, html code.shiki .sb9H8{--shiki-default:#D19A66;--shiki-default-font-style:italic}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 pre.shiki code .subq3, html code.shiki .subq3{--shiki-default:#98C379}html pre.shiki code .sVC51, html code.shiki .sVC51{--shiki-default:#D19A66}",{"title":45,"searchDepth":67,"depth":67,"links":587},[588,589,590,591,592,593],{"id":29,"depth":67,"text":30},{"id":227,"depth":67,"text":228},{"id":316,"depth":67,"text":317},{"id":382,"depth":67,"text":383},{"id":470,"depth":67,"text":471},{"id":359,"depth":67,"text":567},"2022-10-06","Strange encounters with Python classes.","md",{},"/posts/an-unexpected-encounter-with-python-class-attributes",{"title":5,"description":595},"2.posts/20221006.An Unexpected Encounter with Python Class Attributes",null,"aZjMXSm3aCS5Yeh4KW5_wWTonU1rjPDFMO8zmms4Tws",1775234851922]