diff --git a/docs/changelog.rst b/docs/changelog.rst
index 0c6043a..5e919bc 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -43,6 +43,10 @@ The following aliases were removed:
     - :py:meth:`get_random_state()` now represents the state of Faker and ``factory_boy`` fuzzy attributes.
     - Add SQLAlchemy ``get_or_create`` support
 
+*Improvements:*
+
+    - :issue:`561`: Display a developer-friendly error message when providing a model instead of a factory in a :class:`~factory.declarations.SubFactory` class.
+
 *Bugfix:*
 
     - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein <https://github.com/PFStein>`_.
diff --git a/factory/base.py b/factory/base.py
index 2f1d122..bd00bf6 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -375,7 +375,7 @@ class FactoryOptions:
         return self.model
 
     def __str__(self):
-        return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__)
+        return "<%s for %s>" % (self.__class__.__name__, self.factory.__name__)
 
     def __repr__(self):
         return str(self)
diff --git a/factory/builder.py b/factory/builder.py
index b494cc3..09153f7 100644
--- a/factory/builder.py
+++ b/factory/builder.py
@@ -225,9 +225,17 @@ class BuildStep:
         return (self.stub,) + parent_chain
 
     def recurse(self, factory, declarations, force_sequence=None):
+        from . import base
+        if not issubclass(factory, base.BaseFactory):
+            raise errors.AssociatedClassError(
+                "%r: Attempting to recursing into a non-factory object %r"
+                % (self, factory))
         builder = self.builder.recurse(factory._meta, declarations)
         return builder.build(parent_step=self, force_sequence=force_sequence)
 
+    def __repr__(self):
+        return "<BuildStep for {!r}>".format(self.builder)
+
 
 class StepBuilder:
     """A factory instantiation step.
@@ -305,6 +313,9 @@ class StepBuilder:
         """Recurse into a sub-factory call."""
         return self.__class__(factory_meta, extras, strategy=self.strategy)
 
+    def __repr__(self):
+        return "<StepBuilder(%r, strategy=%r)>" % (self.factory_meta, self.strategy)
+
 
 class Resolver:
     """Resolve a set of declarations.

'Options' object has no attribute 'pre_declarations' is too ambiguous
#### The problem
Typo incorrectly specifying the Model for a Factory.

#### Proposed solution
Check if any specified factories are actually Models, in which case give a more clear error message.

#### Extra notes
Thanks for the good work on this neat lib!

diff --git a/tests/test_dev_experience.py b/tests/test_dev_experience.py
new file mode 100644
index 0000000..6677b08
--- /dev/null
+++ b/tests/test_dev_experience.py
@@ -0,0 +1,58 @@
+# Copyright: See the LICENSE file.
+
+"""Tests about developer experience: help messages, errors, etc."""
+
+import collections
+import unittest
+
+import factory
+import factory.errors
+
+Country = collections.namedtuple('Country', ['name', 'continent', 'capital_city'])
+City = collections.namedtuple('City', ['name', 'population'])
+
+
+class DeclarationTests(unittest.TestCase):
+    def test_subfactory_to_model(self):
+        """A helpful error message occurs when pointing a subfactory to a model."""
+        class CountryFactory(factory.Factory):
+            class Meta:
+                model = Country
+
+            name = factory.Faker('country')
+            continent = "Antarctica"
+
+            # Error here: pointing the SubFactory to a model, not a factory.
+            capital_city = factory.SubFactory(City)
+
+        with self.assertRaises(factory.errors.AssociatedClassError) as raised:
+            CountryFactory()
+
+        self.assertIn('City', str(raised.exception))
+        self.assertIn('Country', str(raised.exception))
+
+    def test_subfactory_to_factorylike_model(self):
+        """A helpful error message occurs when pointing a subfactory to a model.
+
+        This time with a model that looks more like a factory (ie has a `._meta`)."""
+
+        class CityModel:
+            _meta = None
+            name = "Coruscant"
+            population = 0
+
+        class CountryFactory(factory.Factory):
+            class Meta:
+                model = Country
+
+            name = factory.Faker('country')
+            continent = "Antarctica"
+
+            # Error here: pointing the SubFactory to a model, not a factory.
+            capital_city = factory.SubFactory(CityModel)
+
+        with self.assertRaises(factory.errors.AssociatedClassError) as raised:
+            CountryFactory()
+
+        self.assertIn('CityModel', str(raised.exception))
+        self.assertIn('Country', str(raised.exception))

pip install --editable '.[dev]'
parse_log_pytest
pytest
pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
asgiref==3.7.2
attrs @ file:///croot/attrs_1668696182826/work
bleach==6.0.0
build==0.10.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
check-manifest==0.50
colorama==0.4.6
coverage==7.2.7
cryptography==44.0.2
distlib==0.3.9
Django==3.2.25
dnspython==2.3.0
docutils==0.20.1
-e git+https://github.com/FactoryBoy/factory_boy.git@85543a502eed7a3d5de17669a98e761e92a78add#egg=factory_boy
Faker==18.13.0
filelock==3.12.2
flake8==5.0.4
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
greenlet==3.1.1
idna==3.10
importlib-metadata==4.2.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
isort==5.11.5
jaraco.classes==3.2.3
jeepney==0.9.0
keyring==23.9.3
markdown-it-py==2.2.0
mccabe==0.7.0
mdurl==0.1.2
mongoengine==0.29.1
more-itertools==9.1.0
packaging @ file:///croot/packaging_1671697413597/work
pep440==0.1.2
Pillow==9.5.0
pkginfo==1.10.0
platformdirs==2.6.2
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycodestyle==2.9.1
pycparser==2.21
pyflakes==2.5.0
Pygments==2.17.2
pymongo==4.7.3
pyproject_hooks==1.2.0
pyroma==4.2
pytest==7.1.2
python-dateutil==2.9.0.post0
pytz==2025.2
readme-renderer==37.3
requests==2.31.0
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.8.1
SecretStorage==3.3.3
six==1.17.0
SQLAlchemy==2.0.40
sqlparse==0.4.4
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tox==3.28.0
trove-classifiers==2025.3.19.19
twine==4.0.2
typing_extensions==4.7.1
urllib3==2.0.7
virtualenv==20.16.2
webencodings==0.5.1
zest.releaser==8.0.0
zipp @ file:///croot/zipp_1672387121353/work

name: factory_boy
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
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib_metadata=4.11.3=hd3eb1b0_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=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - asgiref==3.7.2
      - bleach==6.0.0
      - build==0.10.0
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - check-manifest==0.50
      - colorama==0.4.6
      - coverage==7.2.7
      - cryptography==44.0.2
      - distlib==0.3.9
      - django==3.2.25
      - dnspython==2.3.0
      - docutils==0.20.1
      - faker==18.13.0
      - filelock==3.12.2
      - flake8==5.0.4
      - greenlet==3.1.1
      - idna==3.10
      - importlib-metadata==4.2.0
      - isort==5.11.5
      - jaraco-classes==3.2.3
      - jeepney==0.9.0
      - keyring==23.9.3
      - markdown-it-py==2.2.0
      - mccabe==0.7.0
      - mdurl==0.1.2
      - mongoengine==0.29.1
      - more-itertools==9.1.0
      - pep440==0.1.2
      - pillow==9.5.0
      - pkginfo==1.10.0
      - platformdirs==2.6.2
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pymongo==4.7.3
      - pyproject-hooks==1.2.0
      - pyroma==4.2
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - readme-renderer==37.3
      - requests==2.31.0
      - requests-toolbelt==1.0.0
      - rfc3986==2.0.0
      - rich==13.8.1
      - secretstorage==3.3.3
      - six==1.17.0
      - sqlalchemy==2.0.40
      - sqlparse==0.4.4
      - tox==3.28.0
      - trove-classifiers==2025.3.19.19
      - twine==4.0.2
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - virtualenv==20.16.2
      - webencodings==0.5.1
      - zest-releaser==8.0.0
prefix: /opt/conda/envs/factory_boy

tests/test_dev_experience.py::DeclarationTests::test_subfactory_to_factorylike_model tests/test_dev_experience.py::DeclarationTests::test_subfactory_to_model
docs/changelog.rst factory/base.py factory/builder.py
docs/changelog.rst factory/base.py factory/builder.py