Repository: pydantic/pydantic
Pull request: Include extra when iterating over a model
Fixes #6560

skip change file check


Selected Reviewer: @Kludex

Problem statement:
Unexpected behavior with extra fields 
### Initial Checks

- [X] I confirm that I'm using Pydantic V2 installed directly from the `main` branch, or equivalent

### Description

While migrating to v2, I realized that iteration and extra fields were not working as I expected. Before, extra fields would be included when iterating through a model, but I think that has changed since these added fields are stored in model.__pydantic_extra__ and model.__dict__ is not updated with these fields. I am not sure if this is intentional, and there could be a better way to get the same result as before.  Let me know your thoughts. Thanks!

### Example Code

```Python
from pydantic import BaseModel, ConfigDict


class Model(BaseModel):
    a: int
    b: int

    model_config = ConfigDict(extra='allow')


m = Model(a=1, b=2)
setattr(m, 'extra_field', 3)
for field, value in m:
    print(field, value)
print(f"Model Dump: {m.model_dump()}")
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0.2
        pydantic-core version: 2.1.2 release build profile
                 install path: /Users/aracape/development/penne/venv/lib/python3.10/site-packages/pydantic
               python version: 3.10.5 (v3.10.5:f377153967, Jun  6 2022, 12:36:10) [Clang 13.0.0 (clang-1300.0.29.30)]
                     platform: macOS-12.6.3-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```


Selected Assignee: @adriangb

Hints from issue discussion:

Commit message: Include extra when iterating over a model

Patch to review:
diff --git a/pydantic/main.py b/pydantic/main.py
index 7bb3fb00358..8febf52f0ac 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -835,6 +835,7 @@ class MyModel(BaseModel, extra='allow'):
     def __iter__(self) -> TupleGenerator:
         """So `dict(model)` works."""
         yield from self.__dict__.items()
+        yield from (self.__pydantic_extra__ or {}).items()
 
     def __repr__(self) -> str:
         return f'{self.__repr_name__()}({self.__repr_str__(", ")})'
diff --git a/tests/test_main.py b/tests/test_main.py
index cfa7502c472..10dadf55668 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1029,6 +1029,21 @@ class Bar(BaseModel):
     assert dict(m) == {'c': 3, 'd': Foo()}
 
 
+def test_model_iteration_extra() -> None:
+    class Foo(BaseModel):
+        x: int = 1
+
+    class Bar(BaseModel):
+        a: int
+        b: Foo
+        model_config = ConfigDict(extra='allow')
+
+    m = Bar.model_validate({'a': 1, 'b': {}, 'c': 2, 'd': Foo()})
+    assert m.model_dump() == {'a': 1, 'b': {'x': 1}, 'c': 2, 'd': {'x': 1}}
+    assert list(m) == [('a', 1), ('b', Foo()), ('c', 2), ('d', Foo())]
+    assert dict(m) == {'a': 1, 'b': Foo(), 'c': 2, 'd': Foo()}
+
+
 @pytest.mark.parametrize(
     'exclude,expected,raises_match',
     [


Code review comments:
diff_hunk: @@ -835,6 +835,7 @@ class MyModel(BaseModel, extra='allow'):
     def __iter__(self) -> TupleGenerator:
         """So `dict(model)` works."""
         yield from self.__dict__.items()
+        yield from (self.__pydantic_extra__ or {}).items()
path: pydantic/main.py
text: @user1:
```suggestion
        extra = self.__pydantic_extra__
        if extra:
            yield from extra.items()
```

ready for the :seal: when we drop 3.7.

```
               ___
            .-9 9 `\
          =(:(::)=  ;
            ||||     \
            ||||      `-.
           ,\|\|         `,
          /                \
         ;                  `'---.,
         |                         `\
         ;                     /     |
         \                    |      /
          )           \  __,.--\    /
       .-' \,..._\     \`   .-'  .-'
      `-=``      `:    |   /-/-/`
                   `.__/
```