Repository: pydantic/pydantic
Pull request: Improve `BaseModel.__getattr__` to raise a better error in case of AttributeError in model property
Closes https://github.com/pydantic/pydantic/issues/6360
Based on [@samuelcolvin comment](https://github.com/pydantic/pydantic/issues/6360#issuecomment-1617725012)


Selected Reviewer: @dmontagu

Problem statement:
Change in behaviour when a models property raises an attribute error.
### Initial Checks

- [X] I confirm that I'm using Pydantic V2 installed directly from the `main` branch, or equivalent

### Description

Whilst trying to debug an error in my code, I kept getting an error telling me that my property doesn't exist. Code was something like:

```python
class Model(BaseModel):

    @property
    def some_thing()
        AClass.do_something()
```

The _actual_ error we had was that do_something didn't exist in AClass, however the error being raised was that some_thing didn't exist on model, which was weird. This seems to be a change since V2, now getattr is implemented on Base Model. 

Whilst maybe not a *bug*, it makes debugging other errors harder as the actual error isn't shown.

### Example Code

```Python
import pytest
from pydantic import BaseModel

from pydantic.v1 import BaseModel as BaseModelV1


def test_v1_attribute_error():

    class Model(BaseModelV1):

        @property
        def a_property(self):
            raise AttributeError("Internal Error")

    with pytest.raises(AttributeError, match="Internal Error"):
        Model().a_property


def test_v2_attribute_error():

    class Model(BaseModel):

        @property
        def a_property(self):
            raise AttributeError("Internal Error")

    with pytest.raises(AttributeError, match="Internal Error"):
        Model().a_property



test_v1_attribute_error()
test_v2_attribute_error()
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0
        pydantic-core version: 2.0.1 release build profile
                 install path: /Users/oliver/Library/Caches/pypoetry/virtualenvs/sherlock-9z3yAd_C-py3.11/lib/python3.11/site-packages/pydantic
               python version: 3.11.4 (main, Jun 15 2023, 07:55:38) [Clang 14.0.3 (clang-1403.0.22.14.1)]
                     platform: macOS-13.4-arm64-arm-64bit
     optional deps. installed: ['typing-extensions']
```


Selected Assignee: @hramezani

Hints from issue discussion:
Thanks @ollz272 for reporting this.

Yes, In V2, we have `BaseModel.__getattr__` and it handles private and extra attrs.

Not sure how we are going to handle this case.

@samuelcolvin Do you have any idea?
See [this SO question](https://stackoverflow.com/a/24260979/949890) - this is a known problem with `__getattr__`.

The problem is that `sys.exc_info()` doesn't give you information about the `AttributeError`, so there's not really much we can do.

We could do something like

```py
if hasattr(self.__class__, item):
    raise AttributeError(
        f'{type(self).__name__!r} object has an attribute {item!r} - it looks like the property raised'
        ' an AttributeError which this this __getattr__ method  has muted, sorry'
    )
else:
    # this is the current error
    raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
```

It's ugly, but at least it's less confusing than the current behaviour. I don't know if there could be scenarios where the above `if` statement is wrong?

@dmontagu implemented the original `__getattr__`, maybe he has a clever idea?
My suggestion (combined with @hramezani's idea to check hasattr):
```python
                    if hasattr(self.__class__, item):
                        return super().__getattribute__(item)
```
seems to work to raise the underlying AttributeError. Suggested on @hramezani's PR

Commit message: Update tests/test_main.py

Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com>

Patch to review:
diff --git a/pydantic/main.py b/pydantic/main.py
index 2d75c726568..bf3c9e72eaf 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -703,7 +703,14 @@ def __getattr__(self, item: str) -> Any:
                     except KeyError as exc:
                         raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') from exc
                 else:
-                    raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
+                    if hasattr(self.__class__, item):
+                        return super().__getattribute__(item)  # Raises AttributeError if appropriate
+                            f'{type(self).__name__!r} object has an attribute {item!r} - it looks like the property '
+                            'raised an `AttributeError` which this `__getattr__` method has muted, sorry.'
+                        )
+                    else:
+                        # this is the current error
+                        raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
 
     def __setattr__(self, name: str, value: Any) -> None:
         if name in self.__class_vars__:
diff --git a/tests/test_main.py b/tests/test_main.py
index 8f17a71cef0..f2900ba13a7 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -312,6 +312,16 @@ class Model(BaseModel):
         model.b = 2
 
 
+def test_model_property_attribute_error():
+    class Model(BaseModel):
+        @property
+        def a_property(self):
+            raise AttributeError('Internal Error')
+
+    with pytest.raises(AttributeError, match='Internal Error'):
+        Model().a_property
+
+
 def test_extra_allowed():
     class Model(BaseModel):
         model_config = ConfigDict(extra='allow')


Code review comments:
diff_hunk: @@ -703,7 +703,14 @@ def __getattr__(self, item: str) -> Any:
                     except KeyError as exc:
                         raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') from exc
                 else:
-                    raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')
+                    if hasattr(self.__class__, item):
+                        return super().__getattribute__(item)  # Raises AttributeError if appropriate
+                            f'{type(self).__name__!r} object has an attribute {item!r} - it looks like the property '
+                            'raised an `AttributeError` which this `__getattr__` method has muted, sorry.'
+                        )
path: pydantic/main.py
text: @author:
```suggestion
                        return super().__getattribute__(item)  # Raises AttributeError if appropriate
```