ManyToManyField Error message with Incorrect "to" argument

When passing a string as the “to” argument in ManyToManyField, if you make a mistake or have a typo in the name of the related model you get the following AttributeError.

AttributeError: 'str' object has no attribute '_meta'

This is at best, not a particularly useful error message and at worse misleading as it seems to indicate passing a string as the argument will always break.

I believe there should be an update to make this behave similarly to ForeignKey which gives a significantly more useful message:
(fields.E300) Field defines a relation with model 'TypoModel', which is either not installed, or is abstract.

Existing related fields checks were implemented for this exact purpose (e.g. fields.E300, fields.E307) and from a quick look they don’t seem foreign key specific as they treat with all unresolved model references and both ForeignKey and ManyToManyField call lazy_related_operation through their inherited RelatedField.contribute_to_class method.

It’s hard to tell without a complete stack trace what might cause a crash before they can be run though.

FWIW with a minimal project the related field checks are properly warning about the unresolved reference

from django.db import models

class Foo(models.Model):
    pass

class Bar(models.Model):
    foos = models.ManyToManyField("Fo")

results in

SystemCheckError: System check identified some issues:

ERRORS:
app.Bar.foos: (fields.E300) Field defines a relation with model 'Fo', which is either not installed, or is abstract.
app.Bar.foos: (fields.E307) The field app.Bar.foos was declared with a lazy reference to 'app.fo', but app 'app' does notprovide model 'fo'.
app.Bar_foos.fo: (fields.E307) The field app.Bar_foos.fo was declared with a lazy reference to 'app.fo', but app 'app' does not provide model 'fo'

Ok, let me dig in a little more and see what’s causing this

@charettes Ok, it only happens when you have a custom through table and define through_fields. Here is a minimal re-reproduceable example.

class Foo(Model):
    pass

class Bar(Model):
    foos = ManyToManyField(
        to = "Fo",
        through = 'FooBar',
        through_fields = ('bar', 'foo')
    )

class FooBar(Model):
    foo = ForeignKey('Foo', on_delete=CASCADE)
    bar = ForeignKey('Bar', on_delete=CASCADE)

Also I should point out my project is running Django 5.0.7

Here’s the traceback that should have been provided with the report in the first place

  File "django/core/checks/registry.py", line 88, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "django/core/checks/model_checks.py", line 36, in check_all_models
    errors.extend(model.check(**kwargs))
                  ^^^^^^^^^^^^^^^^^^^^^
  File "django/db/models/base.py", line 1691, in check
    *cls._check_fields(**kwargs),
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "django/db/models/base.py", line 1828, in _check_fields
    errors.extend(field.check(from_model=cls, **kwargs))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "django/db/models/fields/related.py", line 1400, in check
    *self._check_relationship_model(**kwargs),
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "django/db/models/fields/related.py", line 1676, in _check_relationship_model
    related_model._meta.object_name,
    ^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute '_meta'

It looks like you ran into and edge case with the through_fields model checks (a feature rarely used). The following patch resolves the issue (against main)

diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 6a9cb12a90..dd4c09a4e3 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1707,13 +1707,18 @@ def _check_relationship_model(self, from_model=None, **kwargs):
                             and getattr(field.remote_field, "model", None)
                             == related_model
                         ):
+                            related_object_name = (
+                                related_model
+                                if isinstance(related_model, str)
+                                else related_model._meta.object_name
+                            )
                             errors.append(
                                 checks.Error(
                                     "'%s.%s' is not a foreign key to '%s'."
                                     % (
                                         through._meta.object_name,
                                         field_name,
-                                        related_model._meta.object_name,
+                                        related_object_name,
                                     ),
                                     hint=hint,
                                     obj=self,

But it’s debatable if through or through_fields checks should be run in the first place when to is invalid as they will necessary be as well. Applying the above produces the following check failures

SystemCheckError: System check identified some issues:

ERRORS:
app.Bar.foos: (fields.E300) Field defines a relation with model 'Fo', which is either not installed, or is abstract.
app.Bar.foos: (fields.E307) The field app.Bar.foos was declared with a lazy reference to 'app.fo', but app 'app' doe not provide model 'fo'.
app.Bar.foos: (fields.E339) 'FooBar.foo' is not a foreign key to 'Fo'.
app.FooBar: (fields.E336) The model is used as an intermediate model by 'app.Bar.foos', but it does not have a foreign key to 'Bar' or 'app.Fo'.

You are welcome for the report

For the record, I don’t plan on creating a ticket or do any follow up here. If you’d like to see it fixed you should follow the documented procedure.

Thank you for putting that on the record. I’ll create a ticket and be sure to follow the procedures. Its been a pleasure.

1 Like