Skip to content

Typing Django Model Reverse Relations

Find models with a ForeignKey field with an argument that looks like 'related_name="billings"'.

It'll look something like this:

Python
salesorder = models.ForeignKey(SalesOrder, related_name="line_items")

There will be other arguments, but you can do a global search for "related_name=".

If you see related_name="+"You can skip that line. The plus tells Django not to setup the reverse relation. Since there's no reverse relation, there's nothing to add type hints for.

Identify receiving model

In the foreign key field, you'll see the first argument is the target model. If the target model is unquoted like above, the model is likely in the same file, or has been imported.

If the file is quoted, such as "salesorders.SalesOrder", you can find that in slate/salesorders/models.py

Adding imports

One major concern here is circular imports. So while we need to import the source model with the foreign key, it can only during the type checking process.

  • Line 1 shows importing TYPE_CHECKING. This will need to be done once for any file we're going to add type hints for.
  • Lines 11-12 show importing things conditionally. This is only needed if we're getting circular import issues.
python
from typing import TYPE_CHECKING

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError, ValidationError
from django.db import models

from slate.filebrowser.models import FileCore, FileNode, FileVer
from slate.notifications.models import Notification
from slate.projects.models import PriceTier, Project

if TYPE_CHECKING:
	from slate.whatever.models import SourceModel

Adding the type hint to the model

Now, in the model, after all our fields, but before any methods, let's add our type hints.

This is the hype hint we're pulling from the source model. We'll be adding this inside our target model.

Lines 9 - 10 are the relevant bits.

python
# class fields
created = models.DateTimeField(editable=False, auto_now_add=True, db_index=True)
updated = models.DateTimeField(editable=False, auto_now=True)

has_shipments = models.BooleanField(default=False)

html_fields = ["notes", "revision_notes"]

if TYPE_CHECKING:
	line_items: models.manager.RelatedManager["SalesOrderLineItem"]

class Meta:
	ordering = ["-created"]
	verbose_name = "Sales Order"

If you need to add more later, just add them to the TYPE_CHECKING block on the model.

Testing

The way to test this is to write a script and have your IDE show you what it things the type is.

We're going to create a fake management command.

Let's create slate/management/commands/type_hint_test.py

bash
cd ~/Sites/Slate/backend
code slate/management/commands/type_hint_test.py

And now let's add a basic code we can plug things into.

python
from django.core.management.base import BaseCommand

class Command(BaseCommand):
	help = 'Template management command. Probably dont actually run this'

	def handle(self, *args, **options):
		from slate.TARGET_APP.models import TargetModel
		obj = TargetModel()

Now when you start typing obj.l... you should see auto completion and the correct type show up.

Autocomplete example