Your country

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

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

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

Attribute lookup and the data model

How Python resolves obj.attr through descriptors, the instance dictionary and the class, when __getattr__ and __getattribute__ run, and how to build a proxy.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 3 interview questions.

What you will learn

  • Trace how Python resolves obj.attr through data descriptors, the instance dictionary, the class and __getattr__
  • Predict what assignment and del do to an instance and to its class
  • Use __getattr__ for missing names and __getattribute__ for every read, without infinite recursion
  • Build a proxy object that forwards attribute access, special methods included

Before you start

What you’ll learn · Before you start

What you will learn

  • Trace how Python resolves obj.attr through data descriptors, the instance dictionary, the class and __getattr__
  • Predict what assignment and del do to an instance and to its class
  • Use __getattr__ for missing names and __getattribute__ for every read, without infinite recursion
  • Build a proxy object that forwards attribute access, special methods included

Before you start

On this page

A program writes 99 into an object’s own dictionary under the name value, and r.value still reads 21.5; it writes 'shadowed' there under the name of a method, and the method is gone. Both results come from attribute lookup, the search Python runs for every obj.name. The default __getattribute__, inherited from object, looks first for a data descriptor: A descriptor that defines __set__ or __delete__, such as a property or a slot. Attribute lookup checks it before the instance's dictionary, so an instance attribute of the same name can never hide it. More in “Descriptors: how properties and methods work” of that name on the class, such as a property, then in the instance’s own dictionary, then for anything else of that name on the class, such as a method or a class attribute. Only when all three come up empty does it raise AttributeError, and only then does Python call __getattr__. These rules belong to Python’s data model, the part of the language reference that says which special method each piece of syntax calls.

Below you will step through that search on one object, watch assignment and del take a shorter path, build lazy settings and a logging proxy, and reproduce the recursion that catches most first attempts at __getattribute__.

A read looks in four places, always in the same order

lookup_order.py gives a class one name of each kind: a property, a method, a plain class attribute, and a name that only __getattr__ can answer. It reads all four, writes four keys straight into vars(r), past the property, and reads them again:

One name of each kind, before and after the instance dictionary is filled Python · lookup_order.py
class Reading:
    unit = "celsius"                 # a plain class attribute

    def __init__(self, value):
        self._value = value

    @property
    def value(self):                 # a property: a data descriptor
        return self._value

    def label(self):                 # a function: a non-data descriptor
        return f"{self.value} {self.unit}"

    def __getattr__(self, name):     # runs only when every other step fails
        return f"<no {name}>"


r = Reading(21.5)
print("before:", r.value, "|", r.label(), "|", r.unit, "|", r.colour)

# Write straight into the instance's own dictionary, past any property:
vars(r)["value"] = 99
vars(r)["label"] = "shadowed"
vars(r)["unit"] = "kelvin"
vars(r)["colour"] = "red"
print("after: ", r.value, "|", r.label, "|", r.unit, "|", r.colour)
print("vars:  ", vars(r))

Output

before: 21.5 | 21.5 celsius | celsius | <no colour>
after:  21.5 | shadowed | kelvin | red
vars:   {'_value': 21.5, 'value': 99, 'label': 'shadowed', 'unit': 'kelvin', 'colour': 'red'}

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

  • Before the instance’s dictionary is filled, every name is found where you would expect: value through the property, label and unit in the class, and colour by __getattr__, because no other step finds it.
  • Afterwards r.value still gives 21.5. A property is a data descriptor, an object on the class with a __set__ method, and data descriptors are checked before the instance’s dictionary.
  • r.label and r.unit now give the instance’s own values. A function in the class is a non-data descriptor: A descriptor that defines only __get__, such as a function or functools.cached_property. An entry of the same name in the instance's dictionary hides it, which is how an instance attribute can shadow a method. More in “Descriptors: how properties and methods work”: its __get__ method turns it into a bound method: What reading a method through an instance gives: the function together with that instance, so calling it later passes the instance in as self. c.step is a bound method; c.step(5) calls it. More in “Classes, objects and __init__”, but it has no __set__, so an instance attribute of the same name hides it. A plain class attribute is hidden in the same way.
  • r.colour no longer reaches __getattr__. The name is found in the second step, and __getattr__ runs only after every step has failed.

The whole search fits in one column of questions, each answered on the class or on the instance:

Reading obj.name checks data descriptors on the class, then the instance dictionary, then the class, then __getattr__.obj.name1. a data descriptor on type(obj)?(a property, a slot)yes: call its __get__2. a key of vars(obj)?yes: use that value3. anything else on type(obj)?(a method, a class attribute)yes: use it; a method is bound4. does type(obj) define __getattr__?yes: call __getattr__(obj, name)AttributeErrornononono

The four places Python looks when it reads obj.name

Text description of the diagram

The diagram is one column of questions, read from top to bottom. Each box says what happens when the answer is yes; a "no" leads to the next box.

  • It starts at the expression obj.name.
  • Step 1: is there a data descriptor called name on the class or one of its bases, such as a property or a slot? If yes, Python calls its __get__ method and uses the result, without looking at the instance's own dictionary.
  • Step 2: is name a key of the instance's own dictionary, the one vars(obj) shows? If yes, that value is used.
  • Step 3: is there anything else called name on the class or its bases, such as a method or a class attribute? If yes, it is used; a function found this way comes back as a bound method.
  • Step 4: does the class define __getattr__? If yes, Python calls __getattr__(obj, name) and uses what it returns.
  • If every answer is no, the read fails with AttributeError.

Steps 1 to 3 are the work of object.__getattribute__; step 4 runs only after it has raised AttributeError.

Now follow the eight reads of the run through it. The lit stage is the one that answers each read: watch the property answer before the instance’s dictionary is consulted, and __getattr__ drop out once colour has a value of its own.

Interactive · stage by stage Eight reads of one Reading, before and after vars(r) is filled Each stage is one question Python asks for the read, from the top. Step through the run of lookup_order.py and watch which stage answers each read.
Eight reads of one Reading, before and after vars(r) is filled, step 9 of 9. Showing A read of r (r.colour), 1. A data descriptor? (no), 2. A key of vars(r)? (yes: 'red'), 3. On the class? (no), 4. Has __getattr__? (not called), AttributeError. In focus: 2. A key of vars(r)?.A read of rr.colour1. A data descriptor?no2. A key of vars(r)?yes: 'red'3. On the class?no4. Has __getattr__?not calledAttributeErrornothing answered

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

Every step as text
  1. r.value, before. The class holds value as a property, a data descriptor, so the first question answers at once. Python calls the property's getter, which returns 21.5.
  2. r.label, before. label is a function on the class. A function has __get__ but no __set__, so it is not a data descriptor, and vars(r) holds only _value. The third question finds the function and binds it to r.
  3. r.unit, before. unit is a plain class attribute. It is not a data descriptor and not in vars(r), so the third question finds it, and the read gives 'celsius'.
  4. r.colour, before. colour is on neither the class nor the instance. The default lookup raises AttributeError, and only then does Python call __getattr__(r, 'colour'), which returns '<no colour>'.
  5. vars(r) is filled. The program writes value, label, unit and colour straight into vars(r), past the property and past every hook. The instance's dictionary now has five keys.
  6. r.value, after. The first question still answers. The property is a data descriptor, so it wins before the 99 in vars(r) is looked at, and r.value gives 21.5 again.
  7. r.label, after. label is not a data descriptor, so the second question answers, with 'shadowed' from vars(r). The method on the class is hidden from this instance.
  8. r.unit, after. The same happens to unit. 'kelvin' in vars(r) hides the class attribute 'celsius'.
  9. r.colour, after. colour is found in vars(r), so the read gives 'red' and __getattr__ never runs. Python calls it only when every other place came up empty.

The first and third steps search the class and its bases in method resolution order (MRO): The order in which Python searches a class and its bases for an attribute, and the order super() follows. Class.__mro__ lists it, from the class itself to object. More in “Multiple inheritance, MRO and mixins”, the order that super() follows, and the Descriptor Guide in the Python documentation writes the whole search out in about twenty lines of pure Python. What a descriptor is, and how properties and methods use one, is the subject of the next lesson. For lookup, one question decides: does the object found on the class define __set__ or __delete__? If it does, it wins over the instance; if it does not, the instance wins.

Key idea

A data descriptor on the class beats the instance’s dictionary, the instance’s dictionary beats everything else on the class, and __getattr__ runs only when all three have failed.

Checkpoint

Check: The order of a read

  1. Question 1 of 2 A class defines price as a property that returns 100. A test writes vars(item)["price"] = 80, straight into the instance's dictionary. What does item.price give now?

    Choose one answer.

    A hint

    Which of the four places does a property live in, and which comes first?

    Show the answer: Question 1 of 2

    Answer: 100

    A property defines __set__ and __delete__, so it is a data descriptor, and data descriptors on the class are checked before vars(item). The read calls the getter and returns 100; the 80 in the dictionary is never consulted, exactly as r.value stayed 21.5 after 99 was written into vars(r).

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    class Box:
        def size(self):
            return 3
    
    
    b = Box()
    vars(b)["size"] = "big"
    print(b.size)
    Show the answer: Question 2 of 2

    Answer: big

    size is a function on the class: it has __get__ but no __set__, so it is a non-data descriptor and is searched only after the instance's dictionary. vars(b) holds 'big' under size, so b.size is 'big', and the method is hidden for this one instance.

Writes and deletes take a shorter path

Assignment does not search the way a read does. It asks one question, whether the class has a data descriptor of that name, and otherwise stores the value in the instance. Step through it on a Reading like the one above. own is vars(r), so every store and every delete shows there:

Interactive · line by line Assignment and del on one reading own is vars(r), the reading's own dictionary. Step through the stores and the delete, and watch which dictionary each one changes.

Program · assign_trace.py

class Reading:
    unit = "celsius"

    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value


r = Reading(21.5)
own = vars(r)
r.unit = "kelvin"
mine = r.unit
theirs = Reading.unit
del r.unit
back = r.unit
r.value = 30

Variables

After line 19: Reading is Reading; r is <Reading object>; own is {'_value': 21.5}; mine is 'kelvin'; theirs is 'celsius'; back is 'celsius'.ReadingReadingr<Reading object>own'_value':21.5mine'kelvin'theirs'celsius'back'celsius'
  • changed in this step

Printed so far

Error AttributeError: property 'value' of 'Reading' object has no setter

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

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

Line run 19
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. class Reading: makes the class Reading. Its body runs only when it is used.
  3. Line 12. r = Reading(21.5) sets r to <Reading object>.
  4. Line 13. own = vars(r) sets own to {'_value': 21.5}. own is the very dictionary that vars(r) returns, so it shows every change to the attributes r keeps itself. So far it holds _value, which __init__ stored.
  5. Line 14. r.unit = "kelvin" r keeps its value, <Reading object>. Key point The class has no data descriptor called unit, so object.__setattr__ stored 'kelvin' in own. Nothing on the class changed.
  6. Line 15. mine = r.unit sets mine to 'kelvin'.
  7. Line 16. theirs = Reading.unit sets theirs to 'celsius'. Read through the class, unit is still 'celsius': an assignment through an instance never reaches the class.
  8. Line 17. del r.unit leaves r as it was. del r.unit removed the entry from own and nothing else, so own holds _value alone again.
  9. Line 18. back = r.unit sets back to 'celsius'. With no unit of its own, r.unit finds the class attribute again: 'celsius'.
  10. Line 19. r.value = 30 stops the program with AttributeError: property 'value' of 'Reading' object has no setter. Key point value is a property, a data descriptor, so the assignment called its __set__ instead of storing in own. This property has no setter, so it refused, and own never saw a value key.

r.unit = "kelvin" landed in own, and Reading.unit kept 'celsius'. del r.unit removed only r’s entry, so the class value showed through again. The last line stopped the program: a property takes over assignment for its name, and this one has no setter. The longer example below repeats those steps and adds a class whose __setattr__ reports every store:

Assignment and del on an instance, and a __setattr__ that sees every store Python · assign_paths.py
class Reading:
    unit = "celsius"

    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value


r = Reading(21.5)
r.unit = "kelvin"                  # stored in r's own dictionary
print(r.unit, Reading.unit)        # the class keeps its value
del r.unit                         # removes r's own entry, nothing else
print(r.unit)                      # the class attribute shows through again

try:
    r.value = 30                   # the property has no setter
except AttributeError as e:
    print("r.value = 30:", e)
try:
    del r.unit                     # r has no unit of its own any more
except AttributeError as e:
    print("del r.unit:", e)


class Audited:
    """Every assignment, those in __init__ included, passes through __setattr__."""

    def __init__(self, owner):
        self.owner = owner
        self.balance = 0

    def __setattr__(self, name, value):
        print(f"  set {name} = {value!r}")
        super().__setattr__(name, value)   # object.__setattr__ does the real store


a = Audited("Asha")
a.balance += 500
print(vars(a))

Output

kelvin celsius
celsius
r.value = 30: property 'value' of 'Reading' object has no setter
del r.unit: 'Reading' object has no attribute 'unit'
  set owner = 'Asha'
  set balance = 0
  set balance = 500
{'owner': 'Asha', 'balance': 500}

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

  • The property’s refusal says why it refused: property 'value' of 'Reading' object has no setter.
  • del through an instance never deletes from the class: the second del r.unit fails although Reading.unit exists.
  • A class can replace the store with __setattr__, which runs for every assignment to its instances, those in __init__ included. Augmented assignment calls it too: a.balance += 500 reads the old value with a normal lookup and then calls __setattr__ with the sum.

Assigning through self inside __setattr__

Inside __setattr__, writing self.balance = value calls __setattr__ again, and again, until the recursion limit stops it with RecursionError. Store with super().__setattr__(name, value), which reaches object.__setattr__ and does the real store.

__delattr__ works the same way for del: inside it, the real delete is super().__delattr__(name).

Checkpoint

Check: Writes and deletes

  1. Question 1 of 2 Reading.unit is "celsius", and r has no unit of its own. What does del r.unit do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises AttributeError, and Reading.unit stays "celsius"

    Without a data descriptor on the class, del r.unit calls object.__delattr__, which removes the key from vars(r). There is no such key, so it raises AttributeError, and Reading.unit keeps 'celsius'.

  2. Question 2 of 2 Audited.__setattr__ prints one line for every store. An existing instance a runs a.balance += 500. How many lines does that one statement print?

    Type a number.

    A hint

    Augmented assignment reads the old value first. Which hook does a read use?

    Show the answer: Question 2 of 2

    Answer: 1 lines

    a.balance += 500 reads the old value with the normal lookup, which does not involve __setattr__, adds 500 and stores the sum once through __setattr__: one line, set balance = 500, as the example printed.

The fallback for missing names runs only after the lookup fails

__getattr__(self, name) is called only after the normal lookup has failed. It costs nothing for attributes that exist and cannot hide them, which makes it the safe hook for names an object does not have yet, such as settings loaded on first use. Step through a small version and watch own, the instance’s dictionary, and the count of loads:

Interactive · line by line A setting loaded on first use, then found where it was kept own is vars(s). Watch it after each read of s.tax, and watch the count of loads.

Program · lazy_trace.py

class Settings:
    def __init__(self, loaders):
        self._load = loaders
        self.loads = 0

    def __getattr__(self, name):
        if name not in self._load:
            raise AttributeError(name)
        self.loads += 1
        value = self._load[name]()
        setattr(self, name, value)
        return value


s = Settings({"tax": lambda: 18})
own = vars(s)
first = s.tax
second = s.tax
loads = s.loads
smtp = hasattr(s, "smtp")

Variables

After line 20: Settings is Settings; s is <Settings object>; own is {'_load': {'tax': <lambda>()}, 'loads': 1, 'tax': 18}; first is 18; second is 18; loads is 1; smtp is False.SettingsSettingss<Settings object>own'_load':{'tax': <lambda>()}'loads':1'tax':18first18second18loads1smtpFalse
  • changed in this step

Printed so far

 

The program prints nothing.

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

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

Line run 20
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. class Settings: makes the class Settings. Its body runs only when it is used.
  3. Line 15. s = Settings({"tax": lambda: 18}) sets s to <Settings object>.
  4. Line 16. own = vars(s) sets own to {'_load': {'tax': <lambda>()}, 'loads': 0}.
  5. Line 17. first = s.tax sets first to 18. Key point tax was in neither own nor the class, so the normal lookup failed and Python called __getattr__. It loaded 18 and stored it with setattr(): own now has tax, and loads is 1.
  6. Line 18. second = s.tax sets second to 18. This read found tax in own, the second place a read looks, so __getattr__ never ran and loads stayed 1.
  7. Line 19. loads = s.loads sets loads to 1.
  8. Line 20. smtp = hasattr(s, "smtp") sets smtp to False. That was the last line, so the program ends. smtp is not a known section, so __getattr__ raised AttributeError, which is exactly what hasattr() turns into False.

The first read of s.tax missed, so __getattr__ loaded the value and stored it with setattr(); the second read found it in own, and loads stayed 1. The fuller version loads two sections and shows what goes wrong when the fallback raises the wrong exception:

Settings sections loaded on first use, and a fallback that leaks KeyError Python · lazy_settings.py
LOADERS = {
    "database": lambda: {"host": "db.internal", "port": 5432},
    "payments": lambda: {"currency": "INR", "retries": 3},
}


class Settings:
    """Loads each section the first time it is read, then keeps it."""

    def __init__(self, loaders):
        self._loaders = loaders
        self.loads = 0

    def __getattr__(self, name):      # called only when the normal lookup fails
        try:
            loader = self._loaders[name]
        except KeyError:
            raise AttributeError(f"no settings section {name!r}", name=name, obj=self) from None
        self.loads += 1
        value = loader()
        setattr(self, name, value)    # cached: the next read finds it in the instance
        return value


s = Settings(LOADERS)
print(s.payments["currency"], s.payments["retries"], s.database["port"])
print("loads:", s.loads, "| own attributes:", sorted(vars(s)))
print(hasattr(s, "smtp"), getattr(s, "smtp", "not configured"))


class LeakySettings(Settings):
    """The same, except that an unknown name lets KeyError escape."""

    def __getattr__(self, name):
        value = self._loaders[name]()
        setattr(self, name, value)
        return value


leaky = LeakySettings(LOADERS)
try:
    hasattr(leaky, "smtp")
except KeyError as e:
    print("hasattr() raised KeyError:", e)
try:
    getattr(leaky, "smtp", "not configured")
except KeyError as e:
    print("getattr() with a default raised KeyError:", e)

Output

INR 3 5432
loads: 2 | own attributes: ['_loaders', 'database', 'loads', 'payments']
False not configured
hasattr() raised KeyError: 'smtp'
getattr() with a default raised KeyError: 'smtp'

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

  • Three reads cost two loads: the second read of s.payments found the stored value in the instance’s dictionary and never reached __getattr__.
  • For a name it cannot provide, __getattr__ must raise AttributeError, because hasattr() and getattr() with a default treat exactly that exception as “no such attribute”. Settings therefore answers False and “not configured”. Passing name= and obj= to AttributeError, which Python accepts since version 3.10, gives tracebacks what they need to suggest a similar name.
  • LeakySettings lets the dictionary’s KeyError escape. Now hasattr() and getattr() with a default raise instead of answering, and any code that asks “is smtp configured?” crashes.

A failed lookup also includes a property whose getter raises AttributeError: Python then calls __getattr__ with the property’s name. Keep that in mind when a class has both and an error names an attribute that plainly exists.

Checkpoint

Check: __getattr__ as a fallback

  1. Question 1 of 2 A __getattr__ looks names up in a dictionary and lets KeyError escape for unknown ones. What does hasattr(s, "smtp") do when there is no smtp section?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It raises KeyError

    hasattr() reads the attribute and answers False only when the read raises AttributeError. A __getattr__ that lets KeyError escape makes hasattr() and getattr() with a default raise too, so a fallback raises AttributeError for every name it cannot provide.

  2. Question 2 of 2 With the Settings class of the trace, a program reads s.tax five times. How many times does the loader run?

    Type a number.

    A hint

    Where is the value after the first read?

    Show the answer: Question 2 of 2

    Answer: 1 times

    Only the first read misses: __getattr__ loads the value and stores it with setattr(). The next four reads find tax in vars(s), the second place a read looks, and never reach __getattr__.

An override of every read sees its own reads too

__getattribute__ replaces the lookup itself. It runs for every attribute read on the instance, found or not, methods included, which makes it the tool for counting or controlling every access, and the easiest special method to get wrong. The first class below counts reads the obvious way; the second counts them correctly:

A counter that recurses, and one that counts every read Python · count_reads.py
class Broken:
    """Counts reads, but reads self.reads to do it."""

    def __init__(self):
        self.reads = 0

    def __getattribute__(self, name):
        self.reads += 1                  # this read calls __getattribute__ again
        return object.__getattribute__(self, name)


try:
    Broken().reads
except RecursionError as e:
    print("Broken:", type(e).__name__)


class Counted:
    """Counts every attribute read that goes through __getattribute__."""

    def __init__(self, price, qty):
        self.price = price
        self.qty = qty
        self.reads = {}

    def __getattribute__(self, name):
        reads = object.__getattribute__(self, "reads")   # the default lookup: no recursion
        reads[name] = reads.get(name, 0) + 1
        return object.__getattribute__(self, name)

    def total(self):
        return self.price * self.qty

    def __repr__(self):
        return f"Counted({self.price}, {self.qty})"


c = Counted(250, 4)
print(c.total(), c.total())
print(c)                                   # repr() finds __repr__ on the class itself
print(object.__getattribute__(c, "reads"))

Output

Broken: RecursionError
1000 1000
Counted(250, 4)
{'total': 2, 'price': 3, 'qty': 3}

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

Follow the calls behind both results. The arrow that loops back is the override calling itself, and the third column is the default lookup, which a correct override uses for every real read:

Interactive · message by message Who calls whom when a class overrides __getattribute__ Step through one read on Broken, then c.total() on Counted. Each arrow is one call or one value handed back; an arrow that loops back is the override calling itself.
Who calls whom when a class overrides __getattribute__, step 11 of 11. Messages so far: Your code to The override: read 'reads'; The override to The override: self.reads; The override to The override: self.reads again; Your code to The override: read 'total'; The override to Default lookup: 'reads'; Default lookup to The override: the reads dict; The override to Default lookup: 'total'; Default lookup to The override: bound method; The override to Your code: total() to call.Your codethe attribute readThe override__getattribute__Default lookupobject.__getattribute__read 'reads'self.readsself.reads againread 'total''reads'the reads dict'total'bound methodtotal() to call__repr__ found on the class

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

Every step as text
  1. Broken().reads. Reading reads on a Broken instance calls type(obj).__getattribute__, the override, with the name 'reads'.
  2. A read inside the read. The override's first line, self.reads += 1, reads self.reads. That is an attribute read of the same instance, so Python calls the override again.
  3. And again. The new call starts with the same line, and so does the next one. Every read needs another read first, so no call ever gets as far as object.__getattribute__.
  4. RecursionError. Python stops the chain at its recursion limit with RecursionError, which the program catches and prints. Broken never counted a single read.
  5. c.total on Counted. Counted's override is called for c.total, as it is for every read of the instance, found or not.
  6. The counter, safely. It reaches its counter with object.__getattribute__(self, 'reads'). That is the default lookup itself, which never calls the override, so there is no second call.
  7. The dict comes back. The default lookup finds reads in vars(c) and returns the dictionary, and the override adds 1 to reads['total'].
  8. The real lookup. Then it asks the default lookup for total itself, which finds the function on the class.
  9. A bound method. The default lookup binds the function to c and hands the bound method back to the override.
  10. Back to your code. The override returns it, and your code calls it. Inside total(), self.price and self.qty are reads of c as well, and each one goes through the override and is counted the same way.
  11. print(c). print(c) needs __repr__. Python looks it up on the class Counted, as it does for every implicit use of a special method, so the override is not called and nothing is counted for it.

  • Broken.__getattribute__ reads self.reads, and that read is itself an attribute read of the instance, so it calls __getattribute__ again, which reads self.reads again, until Python gives up with RecursionError.
  • Counted reaches attributes through object.__getattribute__(self, name), the default lookup, which never calls the override. Every read inside its methods counts: two calls of total() read total twice and price and qty twice each, and the repr read each of them once more.
  • __repr__ itself is missing from the counts. print(c) finds it on the class, as every implicit use of a special method does, so the override never sees that lookup. The last line reads reads through object.__getattribute__ as well, so printing the counts does not change them.

When a class defines both hooks, __getattribute__ runs first and __getattr__ only after it raises AttributeError. Reach for __getattribute__ rarely: it adds a Python function call to every attribute read on its instances, self.__dict__ and the class’s own helpers included, while __getattr__ covers defaults, lazy values and delegation without touching the names that exist. Side by side, the two hooks differ in when they run, what they cost and how they go wrong:

Compared on __getattr__ __getattribute__
When it runs Only for names the normal lookup cannot find For every read on the instance, found or not
Names that exist Never sees them, so their reads cost nothing extra Sees each one, with a Python call per read
Typical use Defaults, lazy values, forwarding to another object Counting or controlling every read
The usual bug Raising something other than AttributeError Reading through self inside it, until RecursionError

Key idea

Inside __getattribute__, every read through self is another call of __getattribute__, so reach the real attributes through object.__getattribute__(self, name).

Checkpoint

Check: __getattribute__ without recursion

  1. Question 1 of 2 Inside __getattribute__(self, name), which expression reads the counter reads without calling the override again?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: object.__getattribute__(self, "reads")

    Every attribute read of the instance, written with a dot or through getattr(), calls type(self).__getattribute__. Only a direct call of object.__getattribute__ skips the override, so a correct override uses it for every read it needs.

  2. Question 2 of 2 Counted counts every read in its __getattribute__. Which of these lookups is not counted?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The lookup of __repr__ when print(c) runs

    The counts came out as total 2, price 3 and qty 3: two calls of total() plus one repr. print(c) found __repr__ on the class without asking the override, so __repr__ never appears among the counts.

A proxy forwards names, but len() looks on the class

A proxy stands in for another object and forwards what it does not handle itself. Wrappers that add logging, access checks or remote calls are proxies, and __getattr__ makes the forwarding a few lines long:

A proxy that logs every name it forwards Python · logging_proxy.py
class LoggingProxy:
    """Wraps any object, forwards attribute access to it and logs each name used."""

    def __init__(self, target):
        # object.__setattr__ stores on the proxy itself, past our own __setattr__.
        object.__setattr__(self, "_target", target)
        object.__setattr__(self, "log", [])

    def __getattr__(self, name):           # names the proxy itself does not have
        self.log.append(f"get {name}")
        return getattr(self._target, name)

    def __setattr__(self, name, value):    # every assignment goes to the target
        self.log.append(f"set {name}")
        setattr(self._target, name, value)

    def __dir__(self):                      # what dir() and tab completion show
        return dir(self._target)


queue = LoggingProxy([30, 10, 20])
queue.append(5)
queue.sort()
print(queue._target, queue.count(10))
print(queue.log)

try:
    len(queue)
except TypeError as e:
    print("len(queue): TypeError:", e)
print("queue.__len__():", queue.__len__())
print("'sort' in dir(queue):", "sort" in dir(queue))


class ListProxy(LoggingProxy):
    """Special methods must be defined on the class, because Python looks them up there."""

    def __len__(self):
        return len(self._target)

    def __iter__(self):
        return iter(self._target)

    def __getitem__(self, index):
        return self._target[index]


q = ListProxy([3, 1, 2])
print(len(q), list(q), q[-1], 2 in q, q.log)

Output

[5, 10, 20, 30] 1
['get append', 'get sort', 'get count']
len(queue): TypeError: object of type 'LoggingProxy' has no len()
queue.__len__(): 4
'sort' in dir(queue): True
3 [3, 1, 2] 2 True []

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

The run took two different routes to the list. Step through both: an attribute read that reaches the list through __getattr__, and len(), which goes straight to the class:

Interactive · step by step Two ways to reach the list behind a proxy Step through queue.append(5), an attribute read, then len(queue), an implicit use of __len__. Watch which boxes each one visits.
Two ways to reach the list behind a proxy, step 6 of 6. Showing queue.append(5), len(queue), The proxy instance, class LoggingProxy (ListProxy: has __len__), __getattr__ (log: 'get append'), What len() gives (3, for ListProxy([3, 1, 2])), The list [30, 10, 20] ([30, 10, 20, 5]). In focus: class LoggingProxy, What len() gives.normal lookupnot foundgetattr(target)type(queue)queue.append(5)an attribute readlen(queue)a special methodThe proxy instance_target and log onlyclass LoggingProxyListProxy: has __len____getattr__log: 'get append'What len() gives3, for ListProxy([3, 1, 2])The list [30, 10, 20][30, 10, 20, 5]

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

Every step as text
  1. An attribute read. queue.append is an ordinary attribute read. The default lookup searches the proxy's own dictionary, which holds only _target and log, and its class, which has no append.
  2. The fallback runs. The normal lookup failed, so Python calls __getattr__(queue, 'append'). It writes 'get append' to the log and reads the name from the target.
  3. Forwarded to the list. getattr(self._target, 'append') returns the list's own bound method, and the call appends 5 to the list.
  4. len(queue). len() does not read an attribute of queue. It looks __len__ up on type(queue), the class LoggingProxy, and never asks the instance or __getattr__.
  5. Nothing on the class. LoggingProxy defines no __len__, so len() raises TypeError: object of type 'LoggingProxy' has no len(). The log did not change, because __getattr__ was never called.
  6. On the class, it works. ListProxy adds __len__, __iter__ and __getitem__ to the class itself. len(q) now finds __len__ where it looks, and ListProxy([3, 1, 2]) has a length of 3.

  • The proxy keeps _target and log in its own dictionary, stored with object.__setattr__ because its own __setattr__ sends assignments to the target. Reads of those two names succeed in the normal lookup and are never forwarded.
  • append, sort and count are not attributes of the proxy, so each read reached __getattr__, was logged and came back as the list’s own bound method.
  • len(queue) failed although the list has a length. Implicit uses of special methods, by len(), iter(), in, indexing, operators, repr() and with, look the method up on the type, skipping the instance, __getattribute__ and __getattr__. Written out as an attribute, queue.__len__() is an ordinary read, so it was forwarded and worked.
  • ListProxy defines the special methods it needs on the class, and len(), list(), indexing and in work. in fell back to __iter__, because the class has no __contains__, and the log stayed empty: none of these went through __getattr__.
  • __dir__ makes dir(queue) list the target’s names, so tab completion in a shell or an editor shows what the proxy can really do.

The rule is deliberate. Methods such as __hash__ and __repr__ exist on every object, classes included, so looking them up through the instance would find the wrong one whenever the object is itself a class; the language reference’s section on special method lookup shows the effect. For a proxy it means writing out, once, the special methods it promises to support.

Checkpoint

Check: Proxies and special methods

  1. Question 1 of 2 A proxy wraps an open file and forwards every name with __getattr__, and its class defines no special methods. Code runs with proxy:. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It fails, because with looks __enter__ and __exit__ up on the proxy's class, which has neither

    with, len(), iter(), in, indexing, operators and repr() all look their special methods up on the type. A proxy that relies on __getattr__ forwards attribute reads only, so it must define __enter__ and __exit__ on its class to be used in a with statement.

  2. Question 2 of 2 To make for x in proxy: work, where must __iter__ be defined?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: On the proxy's class, as ListProxy does

    A for loop calls iter() on the proxy, and iter() looks __iter__ up on type(proxy). ListProxy defines __iter__ on the class, returning iter(self._target), which is why list(q) and in worked for it.

A module can keep an old name working, with a warning

A module can define __getattr__ and __dir__ as plain functions. Python calls the module’s __getattr__(name) when a name is not found in it, which is the usual way to keep an old name working while warning about it:

An old function name that still works, with a warning

Python (main.py) · old_names/main.py

import warnings

import geometry

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    print(round(geometry.area_of_circle(2), 2))   # the old name still works ...
print([str(w.message) for w in caught])         # ... with a warning

print(round(geometry.circle_area(2), 2), dir(geometry))
try:
    geometry.volume
except AttributeError as e:
    print("AttributeError:", e)

Output

12.57
['area_of_circle() is deprecated: use circle_area()']
12.57 ['circle_area']
AttributeError: module 'geometry' has no attribute 'volume'

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

Python (geometry.py) · old_names/geometry.py

"""Shapes. Version 2 renamed area_of_circle() to circle_area()."""
import math
import warnings

_RENAMED = {"area_of_circle": "circle_area"}


def circle_area(radius):
    return math.pi * radius**2


def __getattr__(name):                  # called only for names this module does not define
    if name in _RENAMED:
        new = _RENAMED[name]
        warnings.warn(f"{name}() is deprecated: use {new}()", DeprecationWarning, stacklevel=2)
        return globals()[new]
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


def __dir__():                          # what dir(geometry) lists
    return ["circle_area"]

Output

It printed nothing.

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

geometry.py defines only circle_area(). Reading geometry.area_of_circle fails the normal lookup, so the module’s __getattr__ issues a DeprecationWarning and returns the new function, and stacklevel=2 makes the warning point at the caller’s line instead of the module’s. An unknown name still raises AttributeError, and dir(geometry) lists only the current name. The hook also serves from geometry import area_of_circle, but not code inside geometry.py itself, which reads its own names directly.

Version note

Python 3.15 makes the suggestions in AttributeError messages smarter. When the missing name exists on one of the object’s own attributes, the message suggests the longer path, such as '.inner.area', and on built-in types it recognises method names from other languages, suggesting '.append' for list.push. The examples on this page run on Python 3.14.8.

Checkpoint

Check: A module's __getattr__

  1. One question geometry.py defines circle_area() and a module-level __getattr__. Which of these reaches that __getattr__?

    Choose one answer.

    Show the answer: One question

    Answer: Reading geometry.area_of_circle in another module

    A module's __getattr__(name) runs when a name read from the module object is missing, which includes from geometry import area_of_circle. Names the module defines are found first, and code inside the module never goes through the hook.

Python Online Compiler Give Reading a __setattr__ that prints, then fill vars(r) again: which of the four writes does it see?

Practise: a read-only proxy, then check yourself

The exercise asks for a proxy that forwards every read and refuses every change, with the special methods it needs written on its class. Its sample tests run in your browser.

Exercise · Medium · Python

A read-only proxy that forwards reads and refuses changes

ReadOnlyProxy(target) in readonly.py wraps any object so that code can read from it but cannot change its attributes. Complete the class:

  • Reading an attribute through the proxy returns the target's attribute, methods included: ReadOnlyProxy("asha").upper() is "ASHA". A name the target does not have raises AttributeError, so hasattr() and getattr() with a default keep working.
  • Assigning, adding or deleting any attribute through the proxy raises AttributeError, and the target stays as it was.
  • dir(proxy) lists the target's attributes.
  • len(), iteration, in and indexing work on the proxy whenever the target supports them, and repr(proxy) is ReadOnlyProxy( followed by the target's repr and ).

The proxy controls attribute access, not what methods do: ReadOnlyProxy([1]).append(2) still changes the list, and the tests do not ask you to stop that.

Starter code · readonly.py

class ReadOnlyProxy:
    """Wraps target: reads are forwarded to it; assignments and deletions raise AttributeError."""

    def __init__(self, target):
        self._target = target
The sample tests · test_readonly.py
from readonly import ReadOnlyProxy


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def moved(self, dx):
        return Point(self.x + dx, self.y)


def refuses(action):
    try:
        action()
    except AttributeError:
        return True
    return False


def test_reads_are_forwarded():
    """attributes and methods of the target are read through the proxy"""
    p = ReadOnlyProxy(Point(2, 3))
    assert (p.x, p.y) == (2, 3)
    assert p.moved(5).x == 7
    assert ReadOnlyProxy("asha").upper() == "ASHA"


def test_writes_and_deletes_are_refused():
    """setting, adding or deleting an attribute raises AttributeError and changes nothing"""
    target = Point(2, 3)
    p = ReadOnlyProxy(target)
    assert refuses(lambda: setattr(p, "x", 10)), "p.x = 10 should raise AttributeError"
    assert refuses(lambda: setattr(p, "z", 1)), "p.z = 1 should raise AttributeError"
    assert refuses(lambda: delattr(p, "y")), "del p.y should raise AttributeError"
    assert refuses(lambda: delattr(p, "_target")), "the proxy's own attributes cannot be deleted either"
    assert (target.x, target.y) == (2, 3)
    assert not hasattr(target, "z")
    assert p.x == 2, "the proxy still works after the refused changes"


def test_missing_names():
    """a name the target lacks raises AttributeError, so hasattr() and getattr() with a default work"""
    p = ReadOnlyProxy(Point(2, 3))
    assert not hasattr(p, "colour")
    assert getattr(p, "colour", "none") == "none"


def test_dir_lists_the_target():
    """dir() of the proxy lists the target's attributes"""
    names = dir(ReadOnlyProxy(Point(2, 3)))
    assert "x" in names and "moved" in names


def test_special_methods():
    """len(), iteration, in, indexing and repr() work through the proxy"""
    p = ReadOnlyProxy([10, 20, 30])
    assert len(p) == 3
    assert list(p) == [10, 20, 30]
    assert 20 in p and 40 not in p
    assert p[-1] == 30 and p[0:2] == [10, 20]
    assert repr(p) == "ReadOnlyProxy([10, 20, 30])"
A hint

Store the target with object.__setattr__(self, "_target", target): a plain self._target = target would run your own __setattr__, which refuses every assignment. Forward reads in __getattr__, and remember that len(), iter(), in, [] and repr() look for their special methods on the class, so each one needs its own method on ReadOnlyProxy.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then test all six ideas of the lesson. Practice starts where your answers have been weakest and explains every answer the moment you give it:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 12 Put the places the default lookup checks for obj.name in the order it checks them.

    Give each item its position, from 1 (first).

    Show the answer: Question 1 of 12

    Answer:

    1. A data descriptor of that name on the class or its bases
    2. A key of that name in vars(obj)
    3. Anything else of that name on the class or its bases
    4. The class's __getattr__, if it defines one

    object.__getattribute__ checks for a data descriptor on the class first, then the instance's dictionary, then anything else on the class, such as a method or a class attribute. Only when all three fail does it raise AttributeError, and only then is __getattr__ called.

  2. Question 2 of 12 What does lookup_order.py print?

    What does this program print? Choose one answer.

    class Reading:
        unit = "celsius"                 # a plain class attribute
    
        def __init__(self, value):
            self._value = value
    
        @property
        def value(self):                 # a property: a data descriptor
            return self._value
    
        def label(self):                 # a function: a non-data descriptor
            return f"{self.value} {self.unit}"
    
        def __getattr__(self, name):     # runs only when every other step fails
            return f"<no {name}>"
    
    
    r = Reading(21.5)
    print("before:", r.value, "|", r.label(), "|", r.unit, "|", r.colour)
    
    # Write straight into the instance's own dictionary, past any property:
    vars(r)["value"] = 99
    vars(r)["label"] = "shadowed"
    vars(r)["unit"] = "kelvin"
    vars(r)["colour"] = "red"
    print("after: ", r.value, "|", r.label, "|", r.unit, "|", r.colour)
    print("vars:  ", vars(r))
    Show the answer: Question 2 of 12

    Answer: it prints

    before: 21.5 | 21.5 celsius | celsius | <no colour>
    after:  21.5 | shadowed | kelvin | red
    vars:   {'_value': 21.5, 'value': 99, 'label': 'shadowed', 'unit': 'kelvin', 'colour': 'red'}

    The property is a data descriptor, so it is checked before the instance's dictionary and r.value stays 21.5. The method and the class attribute are found only after the dictionary, so the instance's label and unit hide them, and colour is found in the dictionary before __getattr__ could run.

  3. Question 3 of 12 functools.cached_property defines __get__ but no __set__. Its first read runs the function and stores the result in vars(obj) under the property's own name. Why do later reads not run the function again?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: It is a non-data descriptor, so the stored value in vars(obj), the second step, answers before the class is searched

    A non-data descriptor is hidden by an instance attribute of the same name. cached_property writes its result into vars(obj) under its own name, so from then on the lookup's second step finds the value, exactly as 'shadowed' hid the method label. A property could not cache this way: as a data descriptor it always wins.

  4. Question 4 of 12 Inside __setattr__(self, name, value), which line stores the value without calling __setattr__ again?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: super().__setattr__(name, value)

    super().__setattr__ reaches object.__setattr__, which does the real store. The other three all end in your own __setattr__ again (the first one even stores under the literal name name), so they recurse until RecursionError.

  5. Question 5 of 12 What does this print?

    Read the code, then choose one answer.

    class Counter:
        count = 0
    
    
    c = Counter()
    c.count += 1
    print(Counter.count, vars(c))
    Show the answer: Question 5 of 12

    Answer: 0 {'count': 1}

    c.count += 1 reads count with the normal lookup, which finds the class attribute 0, then assigns 1 through the instance, which stores it in vars(c). From then on c.count is 1 and hides the class attribute, which stays 0.

  6. Question 6 of 12 A class defines __getattr__. When does Python call it for obj.size?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Only after the normal lookup has raised AttributeError, because size is on neither the instance nor its class

    __getattr__ is a fallback. Names that the normal lookup finds never reach it, which is also why it costs nothing for existing attributes. A property's getter that raises AttributeError counts as a failed lookup too.

  7. Question 7 of 12 What does this print?

    Read the code, then choose one answer.

    class Box:
        colour = "red"
    
        def __getattr__(self, name):
            return name.upper()
    
    b = Box()
    b.__dict__["size"] = 3
    print(b.size, b.colour, b.shape)
    Show the answer: Question 7 of 12

    Answer: 3 red SHAPE

    size is in the instance's dictionary and colour is on the class, so the normal lookup finds both. Only shape is missing everywhere, so only that read reaches __getattr__.

  8. Question 8 of 12 A client object must log a warning whenever code reads a setting name that does not exist, and must stay as fast as before for the hundreds of reads of names that do exist. Which hook do you write?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: __getattr__, which runs only for missing names

    __getattr__ is the hook for missing names: the normal lookup answers every existing name without calling it. __getattribute__ would also work, but at the cost of a Python function call on every read of the class.

  9. Question 9 of 12 A class defines both __getattribute__, which calls object.__getattribute__, and __getattr__. Code reads obj.missing, a name that exists nowhere. What runs?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: __getattribute__ first; it raises AttributeError, and then __getattr__ runs

    With both hooks, every read starts in __getattribute__. If it raises AttributeError, as object.__getattribute__ does for a missing name, Python then calls __getattr__ with the same name.

  10. Question 10 of 12 A proxy class defines only __init__ and a __getattr__ that forwards every name to a list. Which of these work on a proxy of [1, 2, 3]? Choose every right answer.

    Choose every answer that is right.

    Show the answer: Question 10 of 12

    Answer:

    • proxy.count(2)
    • proxy.__len__()

    Reading count or __len__ as an attribute is an ordinary lookup, which fails on the proxy and is forwarded by __getattr__. len() and indexing look their special methods up on the proxy's class, which has neither, so they raise TypeError.

  11. Question 11 of 12 repr(Reading) needs a __repr__, and the class Reading is itself an object. Where does Python look the method up, and why not in Reading's own attributes?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: On type(Reading), because Reading.__repr__ is the method for instances of Reading

    Methods such as __repr__ and __hash__ exist on every object, classes included. If repr(Reading) used Reading.__repr__, it would get the method meant for readings. Looking on type(Reading) finds the right one, which is why every implicit special-method lookup goes to the type.

  12. Question 12 of 12 Another module runs from geometry import area_of_circle. geometry.py defines only circle_area() and the module __getattr__ of the lesson. What happens?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: The module's __getattr__ issues a DeprecationWarning, and the name is bound to circle_area

    A module's __getattr__ runs for any read of a missing name from the module object, and from geometry import area_of_circle is such a read. The hook warns and returns circle_area, which the import binds to the old name.

Interview questions

Fresher to 2–5 years · What is the difference between __getattr__ and __getattribute__?

__getattribute__ runs for every attribute read on an instance, whether the attribute exists or not; the default one, object.__getattribute__, performs the normal search through data descriptors, the instance’s dictionary and the class. __getattr__ is a fallback: Python calls it only after that search has raised AttributeError, so it never sees names that exist. Use __getattr__ for defaults, lazy loading and forwarding to another object, where only the missing names matter. Override __getattribute__ only to observe or control every read, and reach the real attributes through object.__getattribute__(self, name) inside it, or each read calls the method again until RecursionError. Neither hook sees the implicit special-method lookups of len(), + or repr(), which go straight to the class.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A wrapper class forwards attribute reads to self._target from __getattr__. It works everywhere until copy.copy() or pickle.loads() meets it, and both die with RecursionError. Explain the chain of calls, and fix the class so that copies and pickles work.

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

  2. Mid to senior

    A class has a property area and a __getattr__ that raises "no such field". Production logs show AttributeError: no such field 'area', although area is plainly defined. What is going on, how do you find the real bug, and how do you stop such errors from being hidden?

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    Your team wrapped a vendor's payment client in a proxy: __getattr__ forwards every attribute and adds retries to methods named in a list. Months later, with client: fails, isinstance checks against the vendor's class fail, and a new vendor method runs without retries. What went wrong in the design, and what would you build instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • obj.name checks for a data descriptor on the class, then vars(obj), then the rest of the class, and calls __getattr__ only when all three fail.
  • A property beats an instance attribute of the same name; a method or a class attribute does not.
  • Assignment and del change the instance’s dictionary unless a data descriptor on the class takes over; through an instance they never change the class.
  • __getattr__ must raise AttributeError for unknown names, or hasattr() and getattr() with a default break.
  • Inside __getattribute__ and __setattr__, reach the real attributes through object.__getattribute__ and object.__setattr__.
  • len(), in, indexing and other syntax look special methods up on the class, so a proxy defines the ones it supports.

Terms in this lesson

attribute lookup
What Python does to evaluate obj.name: the default __getattribute__ looks for a data descriptor on the class, then in the instance's own dictionary, then for anything else on the class, and only when all three fail is __getattr__ called.
proxy
An object that stands in for another and forwards what it does not handle itself, often from __getattr__. Special methods it supports, such as __len__, must be defined on its class, because len() and other syntax look them up there.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

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