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
- How a ForeignKey models a many-to-one relationship
- The on_delete options and what each one does
- Reverse access with related_name and the _set manager
- Using ManyToManyField with add(), remove(), and set()
- How a OneToOneField extends a model like a profile
- Filtering across relationships with double-underscore lookups
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:
- CASCADE — delete the rows that point here too.
- PROTECT — block the delete and raise an error.
- SET_NULL — keep the rows but set the link to null (needs null=True).
- SET_DEFAULT — set the link back to a default value.
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 posts2 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():
- post.tags.add(django_tag) — link one or more tags.
- post.tags.remove(django_tag) — unlink a tag.
- post.tags.set([t1, t2]) — replace the whole set at once.
- post.tags.all() — query every linked tag.
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
| Field | Relationship | Example |
|---|---|---|
| ForeignKey | Many-to-one | Many posts to one author |
| ManyToManyField | Many-to-many | Posts to tags |
| OneToOneField | One-to-one | A 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
- Previous: The ORM: Queries
- Next: Forms — Build, render, and validate HTML forms with Django's forms framework
- Quick reference: Django cheat sheet › Models & ORM
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.