coveralls: 
[![Coverage Status](https://coveralls.io/builds/13518910/badge)](https://coveralls.io/builds/13518910)

Coverage remained the same at 100.0% when pulling **83f3b000306665f385e93e4fecb8daf2ae8c3696 on dirtycoder:add-build-many** into **b81430b959e88963cf2a46145491a30a1de03dd1 on lamenezes:master**.

lamenezes: Thanks for the PR @dirtycoder! :smile: 
lamenezes: Also, would you please document this new functionality to the README? Thanks again @dirtycoder!
coveralls: 
[![Coverage Status](https://coveralls.io/builds/13693764/badge)](https://coveralls.io/builds/13693764)

Coverage remained the same at 100.0% when pulling **783c9cd8cca5fd50a5a671fbff9cb23b3fd07b4c on dirtycoder:add-build-many** into **b81430b959e88963cf2a46145491a30a1de03dd1 on lamenezes:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/13693897/badge)](https://coveralls.io/builds/13693897)

Coverage remained the same at 100.0% when pulling **18afe6a2a2820c1e97996b755b77426a00ddb054 on dirtycoder:add-build-many** into **f811032749a8c0f5b3415d7b25e2b76fcc16b877 on lamenezes:master**.

diff --git a/.gitignore b/.gitignore
index 0ad2f42..ce23a29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ coverage.xml
 *~
 [._]*.s[a-w][a-z]
 .mypy_cache
+.idea
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9947a37..3705fe3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
 -   repo: git@github.com:pre-commit/pre-commit-hooks
-    sha: 516cc9fa72ad09699f2c03ffbd0aa7f60d75b59a
+    sha: v0.9.5
     hooks:
     - id: debug-statements
     - id: trailing-whitespace
diff --git a/README.rst b/README.rst
index 1341afd..b0deefc 100644
--- a/README.rst
+++ b/README.rst
@@ -14,9 +14,9 @@ plenty of lists and dicts.
 
 It has simple objectives:
 
-- Define your fields easily (just a tuple, nor dicts or instances of type classes whatever)
+- Define your fields easily (just a tuple, not dicts or instances of type classes whatever)
 - Support for field validation
-- Conversion to dict
+- Convert to dict
 
 That's it. If you want something more complex there are plenty of libraries and
 frameworks that does a lot of cool stuff.
@@ -114,6 +114,18 @@ easily done using simple-model:
     ('John Doe', 10)
 
 
+Build many models
+-----------------
+
+It's possible to build many models in a single step, it can be done by passing an iterable
+to the `build_many` method.
+
+.. code:: python
+
+    people = [{'name': 'John Doe'}, {'name': 'John Doe II'}]
+    models = Person.build_many(people)
+
+
 Conversion to Dict
 ------------------
 
@@ -149,7 +161,6 @@ It also supports nested models as lists:
     >> person = Person(name='Jane Doe', age=60)
     >> other_person = Person(name='John Doe', age=15)
     >> social_person = MoreSocialPerson(name='Foo Bar', friends=[person, other_person])
-    >> dict(social_person)
     {
         'name': 'Foo Bar',
         'friends': [
diff --git a/simple_model/models.py b/simple_model/models.py
index 0b04d0b..2ba0244 100644
--- a/simple_model/models.py
+++ b/simple_model/models.py
@@ -30,6 +30,17 @@ class BaseModel(Iterable[Tuple[str, Any]]):
         )
         return '{class_name}({attrs})'.format(class_name=type(self).__name__, attrs=attrs)
 
+    @classmethod
+    def build_many(cls, source: Iterable) -> list:
+        if not source:
+            raise ValueError('source should have at least one item')
+
+        keys_sets = [set(d.keys()) for d in source]
+        if any(map(lambda x: x ^ keys_sets[0], keys_sets)):
+            raise ValueError('All elements in source should have the same keys')
+
+        return [cls(**item) for item in source]
+
     def _get_fields(self) -> Iterator[ModelField]:
         for field_name in self.get_fields():
             field_value = getattr(self, field_name)

Allow model creation from list
In order to create models from a list currently we have to do the following:

```python
my_list = [{'foo': 1, 'bar': 2}, {'foo': 0, 'bar': -10}]
models = [MyModel(**data) for data in my_list]
```

It would be nice to have a SimpleModel classmethod that did it for us in order to avoid repetition and minor mistakes:

```python
my_list = [{'foo': 1, 'bar': 2}, {'foo': 0, 'bar': -10}]
models = MyModel.build_many(my_list)
```
diff --git a/tests/test_model.py b/tests/test_model.py
index 6b8d74b..99179c3 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -23,6 +23,15 @@ def nested_model():
     return child
 
 
+@pytest.fixture
+def many_source():
+    return (
+        {'foo': '1 foo', 'bar': '1 bar', 'qux': '1 qux'},
+        {'foo': '2 foo', 'bar': '2 bar', 'qux': '2 qux'},
+        {'foo': '3 foo', 'bar': '3 bar', 'qux': '3 qux'},
+    )
+
+
 def test_base_model(base_model):
     assert base_model.foo == 'foo'
     assert base_model.bar == 'bar'
@@ -337,3 +346,22 @@ def test_dynamic_model_get_fields():
     fields = model.get_fields()
     assert '_private' not in fields
     assert len(fields) == 3
+
+
+def test_build_many(many_source):
+    models = BaseModel.build_many(many_source)
+
+    assert len(models) == 3
+    assert models[0].foo == '1 foo'
+    assert models[1].bar == '2 bar'
+    assert models[2].qux == '3 qux'
+
+
+def test_build_many_empty_iterable():
+    with pytest.raises(ValueError):
+        BaseModel.build_many([])
+
+
+def test_build_many_different_items():
+    with pytest.raises(ValueError):
+        BaseModel.build_many([{'a': 1}, {'b': 2}])

pip install -e .[dev]
parse_log_pytest
pytest
pytest pytest-cov flake8 mypy
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
flake8==7.2.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
mccabe==0.7.0
mypy==1.15.0
mypy-extensions==1.0.0
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pycodestyle==2.13.0
pyflakes==3.3.2
-e git+https://github.com/lamenezes/simple-model.git@f811032749a8c0f5b3415d7b25e2b76fcc16b877#egg=pysimplemodel
pytest @ file:///croot/pytest_1738938843180/work
pytest-cov==6.0.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions==4.13.0

name: simple-model
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.8.0
      - flake8==7.2.0
      - mccabe==0.7.0
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.2
      - pytest-cov==6.0.0
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/simple-model

tests/test_model.py::test_build_many tests/test_model.py::test_build_many_empty_iterable tests/test_model.py::test_build_many_different_items
tests/test_model.py::test_model_fields_validate_allow_empty_error[None] tests/test_model.py::test_model_fields_validate_allow_empty_error[]
tests/test_model.py::test_base_model tests/test_model.py::test_base_model_iter tests/test_model.py::test_base_model_repr tests/test_model.py::test_base_model_get_fields tests/test_model.py::test_base_model__get_fields tests/test_model.py::test_base_model_is_empty_false[False] tests/test_model.py::test_base_model_is_empty_false[0] tests/test_model.py::test_base_model_is_empty_false[foo] tests/test_model.py::test_base_model_is_empty_false[10] tests/test_model.py::test_base_model_is_empty_false[value4] tests/test_model.py::test_base_model_is_empty_false[value5] tests/test_model.py::test_base_model_is_empty_true[] tests/test_model.py::test_base_model_is_empty_true[value1] tests/test_model.py::test_base_model_is_empty_true[value2] tests/test_model.py::test_base_model_clean tests/test_model.py::test_base_model_validate_success tests/test_model.py::test_base_model_validate_fail tests/test_model.py::test_base_model___eq___equals tests/test_model.py::test_base_model___eq___not_equals tests/test_model.py::test_model tests/test_model.py::test_model_fields_allow_empty tests/test_model.py::test_model_fields_allow_empty__all__ tests/test_model.py::test_model__get_fields tests/test_model.py::test_model_get_fields tests/test_model.py::test_model_get_fields_without_fields tests/test_model.py::test_model_get_allow_empty tests/test_model.py::test_model_get_allow_empty_without_fields tests/test_model.py::test_model_validate_empty[False] tests/test_model.py::test_model_validate_empty[0] tests/test_model.py::test_model_fields_field_validation tests/test_model.py::test_model_fields_field_validation_without_raise tests/test_model.py::test_model_fields_field_validation_error tests/test_model.py::test_model_fields_field_validation_error_without_raise tests/test_model.py::test_model_validate_nested tests/test_model.py::test_model_iter_simple tests/test_model.py::test_model_iter_nested_list[list] tests/test_model.py::test_model_iter_nested_list[tuple] tests/test_model.py::test_model_clean_without_clean_method tests/test_model.py::test_model_clean tests/test_model.py::test_model_clean_nested tests/test_model.py::test_model_iter_clean tests/test_model.py::test_model_get_fields_invalid tests/test_model.py::test_dynamic_model_get_fields
README.rst .gitignore simple_model/models.py .pre-commit-config.yaml
README.rst .gitignore simple_model/models.py .pre-commit-config.yaml