Repository: pydantic/pydantic
Pull request: Fixing `__iter__` returning private `cached_property` info
## Change Summary
* Fixing `__iter__` returning private `cached_property` info
* By default, private `computed_field`s have `repr` set to False

<!-- Please give a short summary of the changes. -->

Fix #7499

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [ ] Documentation reflects the changes where applicable
* [ ] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @samuelcolvin

Problem statement:
Private cached properties are included in BaseModel.__iter__
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Attributes whose name has a leading underscore are not treated as fields by Pydantic, and are not included in the model schema. However this doesn't seem to be the case for fields injected by `cached_property`.

### Example Code

```Python
from functools import cached_property

from pydantic import BaseModel


class Model(BaseModel):
    foo: int

    @cached_property
    def _foo(self):
        return -self.foo


m = Model(foo=1)
assert list(k for k, v in m) == ["foo"]

m._foo
keys = list(k for k, v in m)
assert keys == ["foo"], keys
```

Raises `AssertionError: ['foo', '_foo']`


### Python, Pydantic & OS Version

```Text
pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=false
               python version: 3.10.11 | packaged by conda-forge | (main, May 10 2023, 19:01:19) [Clang 14.0.6 ]
                     platform: macOS-13.2.1-arm64-arm-64bit
     optional deps. installed: ['email-validator', 'typing-extensions']
```

Hints from issue discussion:
hmmm, looks like when calling a property with `cached_property`, the `cache_property` manipulates the class `__dict__` directly,  here is the code snippet from `functools.cached_property`

```py
        try:
            cache = instance.__dict__  # <<< get the class __dict__
        except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None
                   # ... some code
                    val = self.func(instance)
                    try:
                        cache[self.attrname] = val  # <<< adds the attribute to the class instance
                    except TypeError:
                        msg = (
                            f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                            f"does not support item assignment for caching {self.attrname!r} property."
                        )
                        raise TypeError(msg) from None
                    # ... some code
```

and when you iterate over your `Model`, the `__iter__` returns the models`__dict__`, that's why it changes and we don't not see it in `Model.model_fields` 

so it doesn't look like a bug, but more like an unexpected behavior from the `cached_property` side 
I'm not sure how this cannot be considered a bug, especially when there is an easy fix: filter out the `__dict__` keys with leading underscore.
@gsakkis, thanks for reporting! I agree - this is definitely a bug. Will open a PR shortly to address this...

Commit message: adding docs update for default repr behavior

Patch to review:
diff --git a/pydantic/fields.py b/pydantic/fields.py
index 46131b6ee9e..97137c2a098 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -7,6 +7,7 @@
 import typing
 from copy import copy
 from dataclasses import Field as DataclassField
+from functools import cached_property
 from typing import Any, ClassVar
 from warnings import warn
 
@@ -991,6 +992,18 @@ def computed_field(__func: PropertyT) -> PropertyT:
     ...
 
 
+def _wrapped_property_is_private(property_: cached_property | property) -> bool:
+    """Returns true if provided property is private, False otherwise."""
+    wrapped_name: str = ''
+
+    if isinstance(property_, property):
+        wrapped_name = getattr(property_.fget, '__name__', '')
+    elif isinstance(property_, cached_property):
+        wrapped_name = getattr(property_.func, '__name__', '')
+
+    return wrapped_name.startswith('_') and not wrapped_name.startswith('__')
+
+
 def computed_field(
     __f: PropertyT | None = None,
     *,
@@ -998,7 +1011,7 @@ def computed_field(
     alias_priority: int | None = None,
     title: str | None = None,
     description: str | None = None,
-    repr: bool = True,
+    repr: bool | None = None,
     return_type: Any = PydanticUndefined,
 ) -> PropertyT | typing.Callable[[PropertyT], PropertyT]:
     """Decorator to include `property` and `cached_property` when serializing models or dataclasses.
@@ -1092,6 +1105,31 @@ def a(self) -> str:
         #> ValueError("you can't override a field with a computed field")
     ```
 
+    Private properties decorated with `@computed_field` have `repr=False` by default.
+
+    ```py
+    from functools import cached_property
+
+    from pydantic import BaseModel, computed_field
+
+    class Model(BaseModel):
+        foo: int
+
+        @computed_field
+        @cached_property
+        def _private_cached_property(self) -> int:
+            return -self.foo
+
+        @computed_field
+        @property
+        def _private_property(self) -> int:
+            return -self.foo
+
+    m = Model(foo=1)
+    print(repr(m))
+    #> M(foo=1)
+    ```
+
     Args:
         __f: the function to wrap.
         alias: alias to use when serializing this computed field, only used when `by_alias=True`
@@ -1099,7 +1137,8 @@ def a(self) -> str:
         title: Title to used when including this computed field in JSON Schema, currently unused waiting for #4697
         description: Description to used when including this computed field in JSON Schema, defaults to the functions
             docstring, currently unused waiting for #4697
-        repr: whether to include this computed field in model repr
+        repr: whether to include this computed field in model repr.
+            Default is `False` for private properties and `True` for public properties.
         return_type: optional return for serialization logic to expect when serializing to JSON, if included
             this must be correct, otherwise a `TypeError` is raised.
             If you don't include a return type Any is used, which does runtime introspection to handle arbitrary
@@ -1118,7 +1157,13 @@ def dec(f: Any) -> Any:
         # if the function isn't already decorated with `@property` (or another descriptor), then we wrap it now
         f = _decorators.ensure_property(f)
         alias_priority = (alias_priority or 2) if alias is not None else None
-        dec_info = ComputedFieldInfo(f, return_type, alias, alias_priority, title, description, repr)
+
+        if repr is None:
+            repr_: bool = False if _wrapped_property_is_private(property_=f) else True
+        else:
+            repr_ = repr
+
+        dec_info = ComputedFieldInfo(f, return_type, alias, alias_priority, title, description, repr_)
         return _decorators.PydanticDescriptorProxy(f, dec_info)
 
     if __f is None:
diff --git a/pydantic/main.py b/pydantic/main.py
index 0c153d22011..d797fd8335d 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -887,7 +887,7 @@ class MyModel(BaseModel, extra='allow'):
 
     def __iter__(self) -> TupleGenerator:
         """So `dict(model)` works."""
-        yield from self.__dict__.items()
+        yield from [(k, v) for (k, v) in self.__dict__.items() if not k.startswith('_')]
         extra = self.__pydantic_extra__
         if extra:
             yield from extra.items()
diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py
index 93926e3e601..a51ae2771dc 100644
--- a/tests/test_computed_fields.py
+++ b/tests/test_computed_fields.py
@@ -402,7 +402,7 @@ def test_private_computed_field():
     class MyModel(BaseModel):
         x: int
 
-        @computed_field
+        @computed_field(repr=True)
         def _double(self) -> int:
             return self.x * 2
 
diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py
index e7f25d63301..cbc754fed9d 100644
--- a/tests/test_private_attributes.py
+++ b/tests/test_private_attributes.py
@@ -4,7 +4,7 @@
 import pytest
 from pydantic_core import PydanticUndefined
 
-from pydantic import BaseModel, ConfigDict, PrivateAttr
+from pydantic import BaseModel, ConfigDict, PrivateAttr, computed_field
 
 
 def test_private_attribute():
@@ -393,3 +393,36 @@ class BaseConfig(BaseModel):
     )
 
     assert module.BaseConfig._FIELD_UPDATE_STRATEGY == {}
+
+
+def test_private_properties_not_included_in_iter() -> None:
+    class Model(BaseModel):
+        foo: int
+
+        @computed_field
+        @functools.cached_property
+        def _foo(self) -> int:
+            return -self.foo
+
+    m = Model(foo=1)
+    assert '_foo' not in list(k for k, v in m)
+
+
+def test_private_properties_not_included_in_repr_by_default() -> None:
+    class Model(BaseModel):
+        foo: int
+
+        @computed_field
+        @functools.cached_property
+        def _private_cached_property(self) -> int:
+            return -self.foo
+
+        @computed_field
+        @property
+        def _private_property(self) -> int:
+            return -self.foo
+
+    m = Model(foo=1)
+    m_repr = repr(m)
+    assert '_private_cached_property' not in m_repr
+    assert '_private_property' not in m_repr


Code review comments:
diff_hunk: @@ -7,6 +7,7 @@
 import typing
 from copy import copy
 from dataclasses import Field as DataclassField
+from functools import cached_property
path: pydantic/fields.py
text: @author:
Still need to add checks for import errors (doesn't work in Python 3.7)

@user1:
You could move the import to around line 1001