Relationships (FK, M2M)

Reviewed & published by Brayan K

In Django, relationships connect one model to another so your data can mirror the real world — a ForeignKey models a many-to-one link (many posts to one author), a ManyToManyField models a many-to-many link (posts to tags), and a OneToOneField models a one-to-one link (a user to a profile).

Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll wire models together with each of the three relationship fields, follow a link forward to the related object, walk it backward with reverse managers, and filter across relationships using the double-underscore syntax.

What You'll Learn in This Lesson

1 ForeignKey: Many-to-One

A ForeignKey creates a many-to-one relationship. Many rows on the model that holds the field point to one row on the target model. The classic example: many Post rows belong to one Author.

Every ForeignKey needs an on_delete argument that tells Django what to do with the dependent rows when the target is deleted. The most common options are:

Here is the real Django model code. Notice related_name, which names the reverse accessor used to go from an author back to their posts:

# blog/models.py
from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name="posts",   # author.posts.all()
    )

Forward access goes from the post to its author: post.author.name. Reverse access goes from the author back to the posts: author.posts.all() (or author.post_set.all() if you never set related_name).

# Real Django uses a database, so here we simulate the many-to-one link
# with plain Python classes to show how forward and reverse access feel.

class Author:
    def __init__(self, name):
        self.name = name
        self.posts = []          # the "reverse" side: author.posts

class Post:
    def __init__(self, title, author):
        self.title = title
        self.author = author     # the ForeignKey: post.author
        author.posts.append(self)

alice = Author("Alice")
Post("Django Basics", alice)
Post("Models 101", alice)

# Forward access: from a post to its one author
first = alice.posts[0]
print("Forward  -> post.author.name:", first.author.name)

# Reverse access: from an author to their many posts
print("Reverse  -> author.posts:", [p.title for p in alice.posts])
print("Alice wrote", len(alice.posts), "posts")

# ✅ Expected output:
# Forward  -> post.author.name: Alice
# Reverse  -> author.posts: ['Django Basics', 'Models 101']
# Alice wrote 2 posts

2 ManyToManyField: Many-to-Many

A ManyToManyField models a many-to-many relationship: each row can link to many rows on the other side, and vice versa. A Post can have many Tag rows, and each Tag can apply to many posts. Django manages a hidden join table for you, so no column appears on either model.

# blog/models.py
class Tag(models.Model):
    name = models.CharField(max_length=50)

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag, related_name="posts")

You manage the links with the relationship manager. The key methods are .add(), .remove(), .set(), and .clear():

Important: the post must be saved (have a primary key) before you can call .add(), because the join table stores its id. You query across the relation the same way in both directions: tag.posts.all() returns every post that uses that tag.

# Simulate a many-to-many link between posts and tags using Python sets.

class Tag:
    def __init__(self, name):
        self.name = name
        self.posts = set()       # reverse side: tag.posts

class Post:
    def __init__(self, title):
        self.title = title
        self.tags = set()        # the ManyToManyField: post.tags

    def add_tag(self, tag):      # mimics post.tags.add(tag)
        self.tags.add(tag)
        tag.posts.add(self)

    def remove_tag(self, tag):   # mimics post.tags.remove(tag)
        self.tags.discard(tag)
        tag.posts.discard(self)

python = Tag("python")
django = Tag("django")

post = Post("Relationships in Django")
post.add_tag(python)
post.add_tag(django)

print("Tags on post:", sorted(t.name for t in post.tags))

# Reverse query: which posts use the 'django' tag?
print("Posts tagged django:", [p.title for p in django.posts])

post.remove_tag(python)
print("After remove:", sorted(t.name for t in post.tags))

# ✅ Expected output:
# Tags on post: ['django', 'python']
# Posts tagged django: ['Relationships in Django']
# After remove: ['django']

3 OneToOneField & Spanning Relationships

A OneToOneField links exactly one row to exactly one row. It behaves like a ForeignKey with unique=True. The textbook use is a Profile that extends Django's built-in User: each user has one profile, each profile belongs to one user.

# accounts/models.py
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)

# Access in both directions:
#   profile.user        -> the related User
#   user.profile        -> the related Profile (reverse, lowercase model name)

Once your models are connected, you can filter across relationships in a single query using the double-underscore (__) syntax. It lets you reach into a related model's fields without writing a join by hand:

# Every post written by an author named "Alice"
Post.objects.filter(author__name="Alice")

# Every post that has a tag named "django"
Post.objects.filter(tags__name="django")

# Authors who have at least one post (spanning the reverse relation)
Author.objects.filter(posts__isnull=False).distinct()
# Simulate a OneToOne profile plus a double-underscore style lookup.

class User:
    def __init__(self, username):
        self.username = username
        self.profile = None          # reverse: user.profile

class Profile:
    def __init__(self, user, bio):
        self.user = user             # OneToOneField: profile.user
        self.bio = bio
        user.profile = self          # enforce the one-to-one link

alice = User("alice")
Profile(alice, bio="Loves Django")

print("user.profile.bio  ->", alice.profile.bio)
print("profile.user.name ->", alice.profile.user.username)

# Imitate Post.objects.filter(author__name="alice"):
posts = [
    {"title": "FK Guide", "author": alice},
    {"title": "M2M Guide", "author": User("bob")},
]

def filter_author_name(rows, name):
    # "author__name" means: follow .author, then read .username
    return [r["title"] for r in rows if r["author"].username == name]

print("author__name='alice' ->", filter_author_name(posts, "alice"))

# ✅ Expected output:
# user.profile.bio  -> Loves Django
# profile.user.name -> alice
# author__name='alice' -> ['FK Guide']

🎯 Your Turn: Complete the ForeignKey

Fill in the blanks so the helper prints a correct ForeignKey definition. The field type should be ForeignKey and the delete rule should be on_delete=models.CASCADE.

# 🎯 YOUR TURN: replace the two ___ blanks.
# 1. field_type should be "ForeignKey"
# 2. delete_rule should be "on_delete=models.CASCADE"

field_type = "___"
delete_rule = "___"

def build_field(field_type, delete_rule):
    return f"author = models.{field_type}(Author, {delete_rule})"

print(build_field(field_type, delete_rule))

# EXPECTED OUTPUT:
# author = models.ForeignKey(Author, on_delete=models.CASCADE)

Common Errors (And How to Fix Them)

❌ TypeError: __init__() missing 1 required argument: 'on_delete'

You declared a ForeignKey (or OneToOneField) without telling Django what to do on delete.

✅ Fix: always pass on_delete, e.g. models.ForeignKey(Author, on_delete=models.CASCADE).

❌ ValueError: needs to have a value for field "id" before this many-to-many relationship can be used

You called post.tags.add(...) before the post was saved, so it has no primary key.

✅ Fix: call post.save() first, then post.tags.add(tag).

❌ AttributeError: 'Author' object has no attribute 'post'

You confused forward and reverse access. The reverse side is a manager, not a single object.

✅ Fix: use the reverse manager: author.posts.all() (or author.post_set.all() without related_name).

📋 Quick Reference

FieldRelationshipExample
ForeignKeyMany-to-oneMany posts to one author
ManyToManyFieldMany-to-manyPosts to tags
OneToOneFieldOne-to-oneA user to a profile

🏆 Mini Challenge: Tag Counter

Build a tiny blog graph with posts and tags, then count how many posts use each tag — mirroring a reverse many-to-many query like tag.posts.count().

# 🏆 Mini Challenge: count posts per tag (a reverse M2M query).

class Tag:
    def __init__(self, name):
        self.name = name
        self.posts = []

class Post:
    def __init__(self, title, *tags):
        self.title = title
        self.tags = list(tags)
        for tag in tags:
            tag.posts.append(self)   # mimics tag.posts (reverse side)

python = Tag("python")
django = Tag("django")
web = Tag("web")

Post("Intro to Django", python, django, web)
Post("Python Tips", python)
Post("Web Routing", django, web)

# tag.posts.count() for each tag
for tag in (python, django, web):
    print(f"{tag.name}: {len(tag.posts)} post(s)")

# EXPECTED:
# python: 2 post(s)
# django: 2 post(s)
# web: 2 post(s)

❓ Frequently Asked Questions

Lesson complete — your models are connected!

You can now model a many-to-one link with ForeignKey and on_delete, a many-to-many link with ManyToManyField using add/remove/set, and a one-to-one link with OneToOneField — and you can follow relationships forward, backward with related_name, and across with double-underscore lookups.

Practice quiz

Which field models a many-to-one relationship?

  • ForeignKey
  • ManyToManyField
  • OneToOneField
  • CharField

Answer: ForeignKey. A ForeignKey models many-to-one: many rows point to one related row.

Which field models a many-to-many relationship?

  • ForeignKey
  • ManyToManyField
  • OneToOneField
  • IntegerField

Answer: ManyToManyField. A ManyToManyField links many rows on each side via a hidden join table.

Which field is used for a one-to-one relationship like a user profile?

  • ForeignKey
  • ManyToManyField
  • OneToOneField
  • SlugField

Answer: OneToOneField. A OneToOneField links exactly one row to one row, behaving like a unique ForeignKey.

Which argument is required on every ForeignKey?

  • related_name
  • null
  • default
  • on_delete

Answer: on_delete. on_delete is required so Django knows what to do with dependent rows on delete.

What does on_delete=models.CASCADE do?

  • Blocks the deletion
  • Deletes the rows that point to the deleted record too
  • Sets the link to null
  • Sets a default value

Answer: Deletes the rows that point to the deleted record too. CASCADE deletes the dependent rows along with the record they point to.

What does related_name control?

  • The attribute used for reverse access from the related side
  • The database table name
  • The field's default value
  • The on_delete behaviour

Answer: The attribute used for reverse access from the related side. related_name names the reverse accessor, e.g. author.posts instead of author.post_set.

Without related_name, how do you access posts from an author?

  • author.posts.all()
  • author.posts_all()
  • author.post_set.all()
  • author.get_posts()

Answer: author.post_set.all(). The default reverse manager is <modelname>_set, e.g. author.post_set.all().

Which ManyToManyField method replaces the entire set of links at once?

  • add()
  • remove()
  • clear()
  • set()

Answer: set(). post.tags.set([t1, t2]) replaces all current links with the given list.

How do you filter posts written by an author named 'Alice'?

  • Post.objects.filter(author__name='Alice')
  • Post.objects.filter(author.name='Alice')
  • Post.objects.filter(author='Alice')
  • Post.objects.get(name='Alice')

Answer: Post.objects.filter(author__name='Alice'). Double-underscore syntax spans the relation: author__name reaches the related field.

On which table does Django add the column for a ForeignKey?

  • The target model's table
  • A new join table
  • The model holding the ForeignKey
  • Both tables

Answer: The model holding the ForeignKey. The FK column (e.g. author_id) lives on the model that declares the ForeignKey.

Continue this course

Frequently asked questions

What is the difference between ForeignKey and ManyToManyField?

A ForeignKey models a many-to-one relationship: many rows on one side point to a single row on the other, like many blog posts written by one author. It adds a single column (the related id) to the table that holds the ForeignKey. A ManyToManyField models a many-to-many relationship where rows on both sides can link to many rows on the other, like posts and tags — one post can have many tags and one tag can apply to many posts. Django stores that with a hidden join table behind the scenes, so no extra column appears on either model.

Why does ForeignKey require an on_delete argument?

Because Django needs to know what should happen to the rows that point at a record when that record is deleted. If you delete an author, what happens to their posts? on_delete=models.CASCADE deletes the posts too, on_delete=models.PROTECT blocks the deletion, on_delete=models.SET_NULL leaves the posts but sets the author to null, and on_delete=models.SET_DEFAULT uses a default value. Since there is no safe universal default, Django makes on_delete required so you decide deliberately.

What is related_name and why would I set it?

related_name controls the attribute used to access the reverse relationship — going from the 'one' side back to the 'many' side. By default Django creates a manager named <modelname>_set (for example author.post_set). Setting related_name='posts' on the ForeignKey lets you write the friendlier author.posts.all() instead. It also avoids clashes when a model has two ForeignKeys to the same target.

When should I use a OneToOneField?

Use a OneToOneField when each row on one side relates to exactly one row on the other, and you want to extend or split a model. The classic example is a user profile: each User has exactly one Profile and each Profile belongs to exactly one User. It behaves like a ForeignKey with unique=True, so it enforces that the link is one-to-one in both directions.