# django-dirtyfields answers which model fields actually changed

> django-dirtyfields is a small Django mixin that compares an instance's in-memory field values with the database and reports the difference, giving you is_dirty() and get_dirty_fields() on any model that inherits DirtyFieldsMixin.

**romgar/django-dirtyfields** — Tracking dirty fields on a Django model

- Repository: https://github.com/romgar/django-dirtyfields
- Stars: 654 · Forks: 107
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/romgar-django-dirtyfields

## Dirty means in-memory and database values differ

The package is called Django Dirty Fields, and the README gives the definition in one line: dirty means that the field in-memory and database values are different. Everything else in the library follows from that sentence.

This is a narrower thing than change tracking in the audit-log sense, and the distinction matters. The library is not recording a history of edits, it is comparing the object you have in Python right now with the row that is in the database, and telling you which fields differ between them. That is a per-instance, per-invocation question, answered without any extra model, table or middleware.

That framing also explains where the library is useful. A Django application routinely has a model instance that has been mutated in memory and is about to be saved, and the question of which fields were touched comes up for partial updates, for audit records, for deciding whether a save is needed, and for telling a user what they just changed. Answering it by hand means keeping a copy of the original values, or writing a diff against a re-fetched row, or trusting that the caller knows which fields the form submitted. This library does the comparison for you.

It is a deliberately small package. The top-level repository entries are a src directory with the code, a tests directory, docs, a ChangeLog.rst, a pyproject.toml, a tox.ini and a MANIFEST.in. There is one dependency, Django itself, and the packaging metadata marks the development status as Production/Stable.

## Two functions do the work

The usage section lists what you need to do, and it is short. You inherit from DirtyFieldsMixin in the model you want to track, and then you use one of two functions on a model instance to know if the instance is dirty and to get the dirty fields.

The two functions are is_dirty(), which returns a boolean, and get_dirty_fields(), which returns the fields and their values. is_dirty() is the cheap question, and get_dirty_fields() is the one that tells you what changed.

The example shows exactly what the return value looks like, and the shape matters more than it first appears:

```python
    >>> model = ExampleModel.objects.create(boolean=True,characters="first value")
    >>> model.is_dirty()
    False
    >>> model.get_dirty_fields()
    {}

    >>> model.boolean = False
    >>> model.characters = "second value"

    >>> model.is_dirty()
    True
    >>> model.get_dirty_fields()
    {'boolean': True, "characters": "first_value"}
```

A freshly created instance is not dirty, because the object was written to the database and the in-memory state matches what was stored. After two attribute assignments the instance is dirty, and the returned dictionary maps each changed field name to the value that was in the database before the change. That is the useful part: the values are the old ones, which is what you want for a change log or an audit entry, because the new value is simply the current attribute value.

The dictionary is keyed by field name and the values are the pre-change ones, so a caller that wants to persist only what changed can take the keys, and a caller that wants to record what changed can take the values.

## One mixin on the model, and the mixin order matters

Adding the library to a model is a two-line change, and the example model shows the pattern:

```python
    from django.db import models
    from dirtyfields import DirtyFieldsMixin

    class ExampleModel(DirtyFieldsMixin, models.Model):
        """A simple example model to test dirty fields mixin with"""
        boolean = models.BooleanField(default=True)
        characters = models.CharField(blank=True, max_length=80)
```

The import is from the dirtyfields package, and the mixin comes first in the class definition, before models.Model. That ordering is the Python convention for mixins that need to cooperate with the base class, and it is worth respecting rather than reordering for style.

The model itself is unremarkable, which is the point: two ordinary fields, a boolean with a default and a character field. The library does not ask you to change your fields, add a mixin to the model manager, register anything in settings, or write a custom save method. There is no configuration in the README at all, which is unusual for a Django library and tells you that the whole feature is this one class and the two methods it adds.

That simplicity has a consequence. Because there is no configuration hook, there is also no way to tell the library which fields to ignore or how to treat particular field types. Whether a field is comparable, and how it should be compared, is decided by the library rather than by configuration, and the full documentation at django-dirtyfields.readthedocs.io is where that behaviour is described in detail.

## Where this sits against Django's own update_fields

Django already offers a way to write only some columns on save, by passing update_fields to the save method. That is the real alternative to this library, and the difference in approach is instructive.

With update_fields, the caller names the fields. The application knows which fields the form submitted, or the code that mutated the instance knows which attributes it assigned, and it passes that list to save. The mechanism is explicit, and the list is a contract between the caller and the ORM.

With django-dirtyfields, the model computes the answer. The caller asks the instance what changed and gets the list back, which means a piece of code that does not know where the change came from, such as a generic view, a serializer or an admin action, can still work out the difference. The cost is that the comparison happens at query time, against the database, rather than being carried along by the code that made the change.

So the choice is between two sources of truth. If your code paths already know what they changed, update_fields is cheaper and needs no extra dependency. If the change can come from anywhere, including code you do not control, the library gives you a single place to ask.

A second alternative exists for people who want history rather than a diff. Audit packages such as django-simple-history record a row per change, which answers questions the dirty fields library does not attempt, such as who changed a value and when. Neither replaces the other, and the dirty fields library is deliberately the smaller of the two.

## The version matrix is declared per Python and Django pair

The README states which combinations are compatible and tested, and the table is worth reading before you install.

On Python 3.11 the supported Django versions are 4.1, 4.2, 5.0, 5.1 and 5.2. On Python 3.12 the list extends to include 6.0 and 6.1. On Python 3.13 the supported versions are 5.1, 5.2, 6.0 and 6.1, and on Python 3.14 they are 5.2, 6.0 and 6.1. In other words the matrix narrows as the interpreter moves forward, which is what you would expect as older Django versions drop support for newer Python releases.

The packaging metadata agrees with the floor. It requires Python 3.11 or later, and its only runtime dependency is Django 4.1 or later. The classifiers list the same Python versions from 3.11 to 3.14 and the same Django versions from 4.1 to 6.1.

The build system is pinned tightly, with setuptools required at 84.0.0 or later and below 85, which is a narrow window and suggests the project is being built and tested against a specific setuptools release. The version number is dynamic in one of two ways: the project metadata marks it dynamic and the setuptools configuration resolves it from the dirtyfields.__version__ attribute, so the version lives in the code rather than in the manifest.

A dependency-groups section in the same file defines a dev group that includes a test group, and the repository carries a tox.ini, which is where the matrix of interpreter and framework combinations is actually exercised.

## Development branch, docs and a changelog without releases

The repository is not archived, and the last push was on 2026-10-17, so the work is current. The default branch is develop, and the changelog link in the packaging metadata points at the develop branch of ChangeLog.rst, which is the project's pattern for recording changes.

There are no GitHub releases. For a package like this, published on PyPI and installed with pip, that is unremarkable: there is no artefact to tag and release, only a version number to publish. The practical implication is that the changelog file is the release history, and it is where a user should look before upgrading.

The other files in the repository tell a consistent story. There is a docs directory feeding a Read the Docs build, a .readthedocs.yaml for its configuration, a SECURITY.md for reporting problems, a MANIFEST.in for the source distribution, a .flake8 configuration for linting, and a .github directory with a tests workflow whose badge appears at the top of the README. Coverage is reported through Coveralls, with the badge pointing at the develop branch, so coverage is tracked against the branch under active development rather than a release branch.

The authorship is also spelled out. The author is Romain Garrigues, who is the maintainer of the project that became the well known Django package, and the maintainer role is held by Lincoln Puzey. The licence is BSD 3-Clause, permissive, with the notice file declared in the packaging metadata and the licence text in the LICENSE file at the root. As always, that is a description of the terms rather than legal advice, and a company with a policy on licences should read the file itself.

## Conclusion

django-dirtyfields fits a Django project that needs to know what a form or a serializer actually altered, whether to write a partial update, to log a change precisely or to decide whether a save is needed at all, and that is willing to add one mixin to a model. It is the wrong choice if a model has deferred or non-persisted fields where the comparison is meaningless, and it is the wrong choice if you would rather use the built-in save(update_fields=...) and pass the list of changed fields yourself, which costs you a diff you would otherwise get for free here. Before adopting it, check the version matrix for your Python and Django combination, since support is declared per pair, and read the ChangeLog because the package has no GitHub releases to review.

## FAQ

### What does django-dirtyfields do?

It tracks dirty fields on a Django model instance, where dirty means the in-memory and database values of a field are different. You inherit from DirtyFieldsMixin and then call is_dirty() or get_dirty_fields() on an instance.

### What does get_dirty_fields() return?

A dictionary mapping each changed field name to the value it held in the database before the change. In the README example, after changing boolean and characters, the result is {'boolean': True, "characters": "first_value"}.

### How do I install django-dirtyfields?

With pip install django-dirtyfields. The package requires Python 3.11 or later and Django 4.1 or later, and the README publishes a compatibility table of tested Python and Django pairs.

### Which Python and Django versions does django-dirtyfields support?

On Python 3.11 the tested Django versions are 4.1 through 5.2, on 3.12 they extend to 6.0 and 6.1, on 3.13 they are 5.1 through 6.1, and on 3.14 they are 5.2, 6.0 and 6.1.

### Is django-dirtyfields different from using save(update_fields=...)?

Yes. With update_fields the caller names the fields to write, while with django-dirtyfields the model computes the difference between its in-memory values and the database, which lets code that did not make the change still discover what was altered.

## Sources

- [Issues](https://github.com/romgar/django-dirtyfields/issues)
- [License: BSD-3-Clause](https://github.com/romgar/django-dirtyfields/blob/develop/LICENSE)
- [README](https://github.com/romgar/django-dirtyfields/blob/develop/README.md)
- [romgar/django-dirtyfields on GitHub](https://github.com/romgar/django-dirtyfields)

---

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