Last modified: Oct 14, 2026

Add Tags to Django Models with django-taggit

Tags give your content a second layer of structure. Categories are rigid, but tags are flexible. A single article can belong to many topics at once.

django-taggit makes this easy. You add one field to a model and get a full tagging system with queries, forms, and admin support.

This guide shows you how to add tags to Django models with django-taggit. You will cover the model field, migrations, queries, and API serialization.

What django-taggit Provides

django-taggit ships a TaggableManager. It behaves like a many-to-many field, but with a simpler API.

The package stores tags in a shared table. That means tag names are reused across models instead of duplicated per object.

You get built-in methods for adding, removing, and filtering tags. You also get automatic form widgets and admin integration.

If you have not set up the package yet, start with this guide on how to install django-taggit first. Then return here for the modeling details.

Adding the Tag Field to a Model

Adding tags takes one import and one line of code. Open your models file and add a TaggableManager instance.

 # models.py from django.db import models from taggit.managers import TaggableManager class Article(models.Model): title = models.CharField(max_length=200) body = models.TextField() # The tags field tags = TaggableManager() def __str__(self): return self.title 

The TaggableManager class creates the relationship between your model and the tag model. It does not appear as a database column on your model's table.

Instead, django-taggit creates a separate join table. That table stores the link between each tag and each tagged object.

You can name the field anything you like. The name tags is conventional, but topics or keywords work just as well.

Running Migrations

After adding the field, create and apply the migration. Django needs to update your model's database schema.

 python manage.py makemigrations python manage.py migrate 

The output confirms the migration was applied.

 Migrations for 'myapp': myapp/migrations/0002_article_tags.py - Add field tags to article Operations to perform: Apply all migrations: admin, auth, contenttypes, myapp, sessions, taggit Running migrations: Applying myapp.0002_article_tags... OK 

The taggit app itself must already be migrated. If you skipped that step during installation, run python manage.py migrate again after adding "taggit" to INSTALLED_APPS.

Adding Tags to Objects

Once the field exists, you can attach tags from Python. Open the Django shell and create a tagged object.

 python manage.py shell 

Then create an article and add tags to it.

 >>> from myapp.models import Article >>> article = Article.objects.create(title="Django Tips", body="...") >>> article.tags.add("django", "python", "web") >>> article.tags.all() [, , ] 

The .add() method accepts one or more tag names. It creates any tags that do not exist and links them to the object.

Calling .add() again with the same name is safe. django-taggit will not create a duplicate.

Removing and Clearing Tags

Removing tags uses the same pattern. Pass one or more names to the remove method.

 >>> article.tags.remove("web") >>> article.tags.all() [, ] 

To remove every tag from an object in one call, use the clear method.

 >>> article.tags.clear() >>> article.tags.all() [] 

The .clear() method unlinks all tags from the object without deleting the tag records themselves. Other objects can still use those tags.

You can also set the full tag list in one operation. Pass a list of names to the set method.

 >>> article.tags.set(["django", "backend", "api"]) >>> article.tags.all() [, , ] 

The .set() method replaces the existing tags with the new list. It is the cleanest way to sync tags from a form submission.

Filtering by Tag

Tags are only useful if you can query them. django-taggit exposes standard Django lookups on the tag field.

 from myapp.models import Article # Articles tagged "django" Article.objects.filter(tags__name="django") # Articles with any of these tags Article.objects.filter(tags__name__in=["django", "python"]) # Articles with both tags from django.db.models import Q Article.objects.filter( Q(tags__name="django") & Q(tags__name="python") ) 

The filter() method uses the double underscore to reach into the tag name. This is the same syntax Django uses for related model lookups.

Chaining two filters also requires both tags to be present. Use the Q object approach when you need to group conditions.

You can list all tags in use with a simple query on the tag model.

 from taggit.models import Tag # Every tag that exists Tag.objects.all() # Tags with usage counts from django.db.models import Count Tag.objects.annotate(num=Count("taggit_taggeditem_items")).order_by("-num") 

The annotate() method adds a count of how many objects use each tag. Sorting by that count produces a popular-tags list.

Tag filtering combines well with other query filters. If your app also filters on dates or categories, this guide on how to filter QuerySets with django-filter shows how to layer those together.

Handling Tag Input in Forms

The tag field appears automatically in any ModelForm. You only need to include the field name in the Meta class.

 # forms.py from django import forms from .models import Article class ArticleForm(forms.ModelForm): class Meta: model = Article fields = ["title", "body", "tags"] 

When the form renders, the tags appear as a single text input. Users type a comma-separated or space-separated list.

The parser handles several formats. Quoted strings group multi-word tags together.

 # Input examples the parser accepts "django python web" # Three separate tags "django, python, web" # Also three separate tags '"machine learning" python' # One multi-word tag and one single tag 

If the input contains no commas or double quotes, the parser splits on whitespace. When commas or quotes are present, it uses more sophisticated rules to preserve grouping.

You can control the delimiter with a custom parser. Set TAGGIT_TAGS_FROM_STRING in your settings file to a dotted path of your parser function.

Displaying Tags in Templates

Rendering tags in a template is a simple loop. Each tag object has a name attribute.

 {% for tag in article.tags.all %} {{ tag.name }} {% endfor %} 

To link each tag to a filtered list page, wrap the name in an anchor element.

 {% for tag in article.tags.all %} {{ tag.name }} {% endfor %} 

The tag.slug attribute gives you a URL-safe version of the tag name. Use it in your URL patterns instead of the raw name.

For pages that show tags on many objects, prefetch the tags in the view. That prevents one query per object.

 def article_list(request): # Prefetch tags to avoid N+1 queries articles = Article.objects.prefetch_related("tags") return render(request, "articles.html", {"articles": articles}) 

The prefetch_related() method batches the tag lookups into a single query. The template loop then runs without hitting the database again.

Tags in the Django Admin

Register your model in the admin and the tag field appears in the edit form automatically.

 # admin.py from django.contrib import admin from .models import Article @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ["title", "tag_list"] def tag_list(self, obj): """Show tags as a comma-separated string in the list view.""" return ", ".join(tag.name for tag in obj.tags.all()) 

You cannot put the tag manager directly in list_display. That raises an exception because of the many-to-many relationship.

The custom method approach solves it. The method runs per row and returns a plain string.

Add prefetch_related("tags") to the admin's queryset for better performance on large lists. Override the get_queryset() method to include it.

Custom Through Models

Sometimes the default through model is not enough. You might need extra data on the relationship, like who added the tag and when.

django-taggit supports custom through models. Define a subclass of TaggedItemBase and pass it to the manager.

 # models.py from django.conf import settings from django.db import models from taggit.managers import TaggableManager from taggit.models import TaggedItemBase class TaggedArticle(TaggedItemBase): content_object = models.ForeignKey("Article", on_delete=models.CASCADE) added_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True ) class Article(models.Model): title = models.CharField(max_length=200) tags = TaggableManager(through=TaggedArticle) 

The through argument tells django-taggit to use your custom model instead of the default one. The rest of the API stays the same.

Custom through models are also required when your content model uses a non-integer primary key. The default through model assumes an integer ID.

When using a custom through model, remove "taggit" from INSTALLED_APPS. The default models are only created when the app is registered.

Serializing Tags with Django REST Framework

DRF cannot serialize the tag manager with a standard ModelSerializer. The package provides a dedicated serializer for this purpose.

 # serializers.py from rest_framework import serializers from taggit.serializers import TaggitSerializer, TagListSerializerField from .models import Article class ArticleSerializer(TaggitSerializer, serializers.ModelSerializer): tags = TagListSerializerField() class Meta: model = Article fields = ["id", "title", "tags"] 

The TaggitSerializer class handles conversion between tag objects and JSON. The TagListSerializerField class defines the tags field.

With this serializer, your API accepts tag arrays in POST and PUT requests.

 # Example request body { "title": "Django Tips", "tags": ["django", "python", "web"] } 

Responses return the same structure, so frontend clients can render tags without extra transformation.

If your API needs authentication alongside tagging, django-taggit works well with any login system. A social login flow can be added later without touching the tag setup.

For API performance tuning, tag queries deserve attention. Loading tags for a list of objects without prefetching causes an N+1 problem. Following the Django Rest Framework best practices for scalable APIs keeps those queries efficient.

Common Mistakes

Forgetting the migration. Adding the field without running makemigrations leaves the database out of sync with your models.

Using the field in list_display. The tag manager cannot be displayed directly. Use a custom method that returns a string.

No prefetching on list pages. Every object triggers its own tag query. Prefetch to keep page loads fast.

Confusing the package and app names. The pip package is django-taggit. The Django app label is taggit. Both are correct in their context.

Ignoring case sensitivity. By default, "Django" and "django" are different tags. Set TAGGIT_CASE_INSENSITIVE in settings to change that.

Using a standard serializer in DRF. The default serializer cannot handle the tag field. Use the TaggitSerializer instead.

Best Practices

Keep tag names short. Long names are hard to display and harder to type.

Normalize input before saving. Strip whitespace and lowercase names if you want consistent data.

Add a tag detail view. Letting users browse by tag turns a simple feature into a real navigation path.

Show tag counts. A count next to each tag tells users how much content exists before they click.

Limit tag input length. Enforce a maximum character count in your forms to prevent abuse.

Test your tag queries. Filters that look correct can silently return wrong results after a schema change.

Conclusion

Adding tags to Django models with django-taggit takes one field and one migration. From there, the package handles everything else.

You get a clean API for adding, removing, and querying tags. Forms and the admin work automatically. Django REST Framework integration needs one extra serializer class.

When the defaults are not enough, custom through models and custom parsers are available without leaving the package.

With tags in place, your content becomes easier to organize, search, and discover. That is a lot of value from a single line in your model.