# django-sortedm2m: giving a Django ManyToMany relation a remembered order

> Django's ManyToManyField stores no order for the rows you add, which breaks gallery style relations. django-sortedm2m adds an ordering column to the intermediate table and ships a drag and drop admin widget, with a hand written migration step when you convert an existing field.

**jazzband/django-sortedm2m** — A transparent sorted ManyToMany field for django.

- Repository: https://github.com/jazzband/django-sortedm2m
- Website: https://pypi.org/project/django-sortedm2m/
- Stars: 524 · Forks: 147
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/jazzband-django-sortedm2m

## The rows you add to a ManyToManyField come back in no order at all

The scenario this project is built around is small and common. You have a Gallery model and a Photo model. A photo may belong to many galleries, and a gallery holds many photos, so a many to many relation is the natural choice. The trouble starts as soon as anyone cares about the sequence. Django's own ManyToManyField writes a row into an intermediate table for each pair, and that table carries no column that records when a row was added. Query the relation back and the database is free to return the pairs in whatever order the planner finds cheapest, so the gallery ends up with an arbitrary sequence rather than the one you built. The documentation calls this a random ordering and argues it is unsuitable in most cases. That is the whole problem statement. django-sortedm2m offers a single field class, SortedManyToManyField, that behaves like the Django original but remembers the order in which relations were added. The field is used in place of ManyToManyField, so most code that touches the relation keeps working, including the familiar add() call.

## The intermediate table gains a sort_value column you can rename

The mechanism is one extra column in the intermediate table that holds the ordering information, so the sequence lives in the database rather than in a cache or an in memory list. The field accepts a few arguments that control this behaviour. The first is sorted, which defaults to True. Setting sorted to False forces the field to behave like Django's original ManyToManyField: no ordering is performed on the relation, and the intermediate table has no database field for storing ordering information at all. That is the escape hatch for teams who want the field's interface now and the overhead later. The second argument is sort_value_field_name, which defaults to 'sort_value'. It names the column in the intermediate table by which the relationship is ordered, and the documentation gives the reason it exists: you can change it when you have a legacy database that you need to integrate into your application. The third is base_class, which defaults to None and points at the base class of the through model.

## Swapping the field into a model and adding photos in order

Installation steps are not spelled out in the project documentation, so take the package from its PyPI page at pypi.org/project/django-sortedm2m and then declare the field the way you would declare Django's own. The project describes itself as a drop in replacement for ManyToManyField, so the import path and the constructor call look familiar. This is the model pattern the documentation gives, and you should see the relation declared on the gallery model exactly as written:

```python
    from django.db import models
    from sortedm2m.fields import SortedManyToManyField

    class Photo(models.Model):
        name = models.CharField(max_length=50)
        image = models.ImageField(upload_to='...')

    class Gallery(models.Model):
        name = models.CharField(max_length=50)
        photos = SortedManyToManyField(Photo)
```

Ordering is a side effect of how relations are attached, so the code that attaches them has to preserve sequence. The documentation shows a loop that adds photos one at a time in a chosen order, and it is the order of those add() calls that the field records:

```python
    gallery = Gallery.objects.create(name='Photos ordered by name')
    for photo in Photo.objects.order_by('name'):
        gallery.photos.add(photo)
```

Because the order comes from the sequence of calls, code that inserts rows in a different order, for instance sorting the photos by name before attaching them, produces a different gallery. That is a deliberate trade: the field gives you control over order and takes away any guarantee that a later query will reorder rows for you.

## Converting an existing ManyToManyField takes two edits and one import

If a project already has a ManyToManyField and wants the ordered variant, the plain Django migration machinery does not do the job on its own. The documentation states that a migration created by makemigrations will not work as expected for this conversion, and the reason is structural: the intermediate table needs a new column, existing rows need default values filled in, and Django's own AlterField operation does not know about that. The documented procedure is to change the field in the models file, run manage.py makemigrations, then edit the generated migration file and change migrations.AlterField in the operations list to AlterSortedManyToManyField, importing that operation from sortedm2m.operations. The documentation states that this operation takes care of changing the intermediate tables, adding the ordering field and filling in default values. The reverse direction, moving from SortedManyToManyField back to ManyToManyField, is covered by the same procedure. This is the part of the project that costs real time, because a generated migration that is submitted unchanged either fails outright or leaves the schema in a state the field does not expect. Existing rows arrive with default ordering values, so the gallery order after the change is the order of whatever the default sort produced, not the order your editors had in mind.

## The admin widget needs sortedm2m in INSTALLED_APPS and no filter_horizontal

For editors who need to rearrange items by hand, the field renders a custom widget that shows the selected entries as a list of checkboxes and lets them be sorted by drag'n'drop. The widget depends on static css and js files that ship with the package, so the app has to be registered in the settings, otherwise the admin cannot find those files and the sorting behaviour is missing. The documented settings entry looks like this:

```python
   INSTALLED_APPS = (
       'django.contrib.auth',
       'django.contrib.contenttypes',
       'django.contrib.sessions',
       'django.contrib.sites',
       'django.contrib.messages',
       'django.contrib.staticfiles',
       'django.contrib.admin',

       'sortedm2m',

       '...',
   )
```

There is a second admin condition worth stating plainly. The documentation asks you to make sure not to have the model listed in any filter_horizontal or filter_vertical tuples inside your ModelAdmin definitions, because those widgets render a different interface that offers no place to arrange the sequence. The result of getting both parts right is a checkbox list in the change form where an editor can drag a selected photo to a new position, and the stored relation follows that arrangement.

## raw_id_fields is the fallback when the drag and drop widget does not fit

If the field has to stay in a form where the horizontal or vertical filters are in play, there is a second route. The field also works with the admin raw_id_fields option: add the name of the SortedManyToManyField to that list and Django renders a simple text input instead of the drag and drop list. The documentation states that the order in which the ids are entered into the input box is used to sort the items of the relation, so a set of numeric ids typed in the intended sequence becomes the ordering.

```python
    from django.contrib import admin

    class GalleryAdmin(admin.ModelAdmin):
        raw_id_fields = ('photos',)
```

This path suits large relations where a checkbox list would be unreadable, and it is also the only ordering interface described for those forms. The cost is that the order depends on a human typing ids in the right sequence, with no visual confirmation of what the gallery will look like, and a typo in an id is invisible until the relation is loaded.

## Extra fields on the through model sit outside the migration machinery

This is the case where the project is the wrong tool, and its own documentation says so. The base_class argument accepts an abstract base class for the through model, and the documentation notes that you could use it to add additional fields to that model. The warning follows immediately: those fields will not be created or modified by an automatically created migration, and you will need to take care of migrations yourself. The documentation's own recommendation is to consider not using sortedm2m in that case, and to use an ordinary Django ManyToManyField with your own through model instead. That alternative approach moves the ordering responsibility onto you: you name the column, you write the migrations, you decide what happens when rows are reordered or inserted in the middle. The trade is that you get full control over the intermediate table and pay for it with manual schema work, while sortedm2m automates the common case and punishes you when you leave it. A related, smaller trap is the base_class option used only to improve the string representation of sortedm2m relations through a __str__ method, which is harmless.

## The last push is recent, the last published release is not

The repository is not archived and the last push was on 2026-10-20, so the source tree is being worked on. The releases tell a different story about what you install. The most recent published release is 4.0.0, dated 2024-08-11, and before that came 3.1.1 from 2021-09-30 and 3.1.0 from 2021-08-31. In other words, code moves on master while the published package has not been cut since August 2024. Anyone pinning the released version is installing code that predates the most recent commits, and anyone working from master is running something that no release has promised to be stable. The setup.py metadata lists Django 4.2, 5.0 and 5.1 among the supported framework versions, and the project requires Python 3.6 or newer, with the exact tested Django versions kept in the .github/workflows/test.yml configuration. The package is published under the BSD-3-Clause licence, which is permissive and imposes no copyleft obligation on code that links to it, though that is not legal advice and your own reading of LICENSE.txt in the repository is the last word. Development in this repository is organised around a Makefile with a quality target that runs isort, pycodestyle and pylint over the example, sortedm2m, sortedm2m_tests and test_project directories, and the contributor notes point at tox for running the suite across Python versions, installed with pip install tox.

## Conclusion

Adopt django-sortedm2m when a many to many relation genuinely needs a hand arranged order, such as photos in a gallery, and only if you are willing to run makemigrations and then edit the generated file to swap AlterField for AlterSortedManyToManyField. Skip it for plain relations, where the extra column and the extra migration step cost more than the ordering is worth, and for models that need extra fields on the through table, where the project itself points you at a plain ManyToManyField with your own through model. Before you start, check that the intermediate table and the sort_value column name fit your existing schema, since renaming them is supported but affects every migration that touches that table.

## FAQ

### Is django-sortedm2m a drop-in replacement for Django's ManyToManyField?

The documentation describes it as a drop-in replacement: SortedManyToManyField behaves like the original and remembers the order in which relations were added. The one manual step is the conversion migration needed when a project already has a ManyToManyField.

### How do I migrate an existing ManyToManyField to a SortedManyToManyField in Django?

Change the field in the models file, run manage.py makemigrations, then edit the generated migration and replace migrations.AlterField with AlterSortedManyToManyField, imported from sortedm2m.operations. That operation changes the intermediate tables, adds the ordering field and fills in default values.

### Can I set the name of the ordering column in the intermediate table?

Yes. The sort_value_field_name argument defaults to 'sort_value' and names the column by which the relationship is ordered. The documentation suggests changing it when you need to integrate with a legacy database.

### Why does the admin not offer drag and drop sorting for my relation?

Two conditions apply. The package must be added to INSTALLED_APPS so the css and js files it needs can be found, and the model must not appear in any filter_horizontal or filter_vertical tuple in your ModelAdmin definitions.

### Can I add fields of my own to the intermediate table with this package?

The base_class argument can point the through model at an abstract base class, but any extra fields you add there will not be created or modified by an automatically generated migration, so you handle those migrations yourself. The documentation suggests using a plain ManyToManyField with your own through model in that situation instead.

## Sources

- [jazzband/django-sortedm2m on GitHub](https://github.com/jazzband/django-sortedm2m)
- [License: BSD-3-Clause](https://github.com/jazzband/django-sortedm2m/blob/master/LICENSE)
- [Project website](https://pypi.org/project/django-sortedm2m/)
- [README](https://github.com/jazzband/django-sortedm2m/blob/master/README.md)
- [Releases](https://github.com/jazzband/django-sortedm2m/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jazzband-django-sortedm2m
