# django-imagekit turns a resize into a model field, and the cache path tells you how it works

> A Django app with two runtime dependencies that generates derived images on demand. The repository description still says v6.0 while the newest tag is 6.1.1, and 91 open issues sit next to 2,347 stars.

**matthewwithanm/django-imagekit** — Automated image processing for Django. Currently v6.0

- Repository: https://github.com/matthewwithanm/django-imagekit
- Website: http://django-imagekit.rtfd.org/
- Stars: 2,345 · Forks: 277
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/matthewwithanm-django-imagekit

## The whole idea in one sentence

The README opens with the clearest statement of intent this project has: ImageKit is a Django app for processing images. Need a thumbnail? A black-and-white version of a user-uploaded image? ImageKit will make them for you. If you need to programmatically generate one image from another, you need ImageKit.

That last sentence is the whole scope. This is not an image hosting service, not a CDN, and not a media management UI. It is a Python layer that takes a source image plus a list of processors and gives you a derived image with a URL, either as a model field or as a class you call from a view.

The conceptual model is one term. The docs call an image spec a type of image generator that generates a new image from a source image. Everything else in the library is a way of declaring one of those specs and deciding where the output goes. There are two ways to declare a spec, as a field on a model and as a standalone class, and there is a third route through template tags for the case where you do not want to touch models at all.

The project metadata is modest in a way that predicts the design. Language Python, licence BSD-3-Clause, 2,347 stars, 276 forks, and a declared homepage at django-imagekit.rtfd.org. The default branch is develop, not master or main, and the build badge in the README is pinned to that branch, which means the badge you see reflects the branch where work happens rather than the branch where releases are cut.

## Two runtime dependencies, and Pillow is not one of them

Here is the dependency story, taken from pyproject.toml. The runtime list is two entries long:

```
dependencies = [
    "django-appconf",
    "pilkit",
]
```

That is worth pausing on. django-appconf is the mechanism that gives an app settings with defaults under an AppConf prefix, which is how ImageKit exposes its cache configuration. pilkit is the actual image processing toolkit, and it is where the processor implementations live. ImageKit itself is the Django integration layer on top of those two.

Pillow is deliberately not in that list, and the README makes step one of installation Install Pillow. The reasoning in the README is that if you are using an ImageField in Django you should have already done this, which is true because Django itself requires Pillow for ImageField. So Pillow arrives through Django's dependency rather than through ImageKit's. The practical consequence is that in an environment where ImageField is not used, nothing will drag Pillow in for you and you will get an import failure at the moment you first process an image, not at install time.

The optional extras are where the interesting integration choices show up. There are three separate async backends:

```
async = ["django-celery>=3.0"]
async_rq = ["django-rq>=0.6.0"]
async_dramatiq = ["django-dramatiq>=0.4.0"]
```

Three optional dependency groups for three different task queues, which tells you that deferred thumbnail generation is a first class use case rather than a bolt on. The test extra pulls in pytest, pytest-django, pytest-cov, Pillow and beautifulsoup4, that last one being for asserting on rendered template output.

Two more pyproject details matter for packaging. The version is dynamic, resolved from an attribute rather than hardcoded:

```
[tool.setuptools.dynamic]
version = {attr = "imagekit.pkgmeta.__version__"}
```

So there is exactly one place in the package where the version string lives, imagekit.pkgmeta, and the build reads it. That is the right arrangement and it also means a fork has to update that module to cut a different version. The package also sets include-package-data = true and zip-safe = false, the second of which is a hint that the library expects to be installed into a real filesystem rather than imported from inside a zipped egg, which makes sense for something that writes files.

## ImageSpecField is a virtual column, and that single word decides your migrations

The model field example is the one the README leads with, and it is short:

```
    from django.db import models
    from imagekit.models import ImageSpecField
    from imagekit.processors import ResizeToFill

    class Profile(models.Model):
        avatar = models.ImageField(upload_to='avatars')
        avatar_thumbnail = ImageSpecField(source='avatar',
                                          processors=[ResizeToFill(100, 50)],
                                          format='JPEG',
                                          options={'quality': 60})
```

The shape reads like a Django ImageField with extra keywords. source points at the field to read, processors is the transformation list, format is the output encoding, and options is passed through to the encoder, here as a JPEG quality of 60.

The note under this example is the part that saves you an afternoon. ImageSpecFields are virtual, they add no fields to your database and don't require a database, which means adding one to a model produces no migration and no column. Because there is no column, the path to the generated file has to be computed from the source image and the spec rather than stored. The README example shows the shape of that path:

```
    print(profile.avatar_thumbnail.url)    # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
    print(profile.avatar_thumbnail.width)  # > 100
```

Read the URL and the design becomes clear. The filename is not a name you chose, it is a 32 character hex digest under a CACHE directory inside your media root. That is a content addressed cache, and it means the same source image with the same processor settings always maps to the same file, with no database lookup and no collision handling. It also means the generated files live in your media directory, so whatever serves /media has to serve /media/CACHE too, and if your storage backend is remote the cache directory goes wherever the source files go.

Now the sibling field, ProcessedImageField, which is the opposite choice:

```
    from django.db import models
    from imagekit.models import ProcessedImageField
    from imagekit.processors import ResizeToFill

    class Profile(models.Model):
        avatar_thumbnail = ProcessedImageField(upload_to='avatars',
                                               processors=[ResizeToFill(100, 50)],
                                               format='JPEG',
                                               options={'quality': 60})
```

No source, because it is not processing another field, and an upload_to argument, because it behaves exactly as it does for Django ImageFields. The README's own note makes the consequence explicit: ProcessedImageFields really are just like ImageFields, they save the file path in the database and you need to run syncdb or create a migration when you add one to your model. So ProcessedImageField stores a row and keeps the original if you kept it, while ImageSpecField stores nothing and derives everything. If you want the original gone, that is the field you want.

## Specs outside models, which is the path most teams end up on

The second route is a plain class, and the README is direct about when to use it: sometimes you can't (or don't want to) add fields to your models, and that's okay. It calls out view code specifically, particularly when the processing being done depends on user input.

```
    from imagekit import ImageSpec
    from imagekit.processors import ResizeToFill

    class Thumbnail(ImageSpec):
        processors = [ResizeToFill(100, 50)]
        format = 'JPEG'
        options = {'quality': 60}
```

Same capability as the field version. What the class does not define is what source the spec is acting on, or what should be done with the result. Both are supplied at call time:

```
    source_file = open('/path/to/myimage.jpg', 'rb')
    image_generator = Thumbnail(source=source_file)
    result = image_generator.generate()
```

generate() returns a file-like object, not a saved file and not a Django model instance. The README shows both halves of what you can do with it. Save it yourself:

```
    dest = open('/path/to/dest.jpg', 'wb')
    dest.write(result.read())
    dest.close()
```

or feed it to something else, since the README notes you don't have to use open and can use whatever File-like object you want, including a model's ImageField. That is the same duck typing Pillow expects, so anything PIL accepts will work here.

Templates are the third route, and they are the one that keeps image processing out of your model layer entirely. For fields, the template usage is ordinary Django, an img tag pointing at the url attribute. For standalone specs you register the generator so the template can name it:

```
    from imagekit import ImageSpec, register
    from imagekit.processors import ResizeToFill
```

Both spec classes define the same thing. The README makes the point that you can generate processed image files directly in your template from any image without adding anything to your model, which is the property that makes this library usable in an existing project where touching models is politically expensive.

## The version story has three places to disagree

Here is the part worth slowing down on, because the release history and the repository metadata tell slightly different stories and both are current.

The repository description reads Automated image processing for Django. Currently v6.0. The most recent release is 6.1.1, published on 15 September 2026. So the one line of text that GitHub, package indexes and search engines surface is a version behind the actual latest tag. If you are pinning a version from a search result rather than from your package manager, you will pin the wrong one.

The 6.0 release, dated 12 October 2025, is the one that explains the compatibility floor, and it is unusually well written for a major version note. It states the supported Python versions as 3.9 through 3.14 and the supported Django versions as 3.2, 4.2, 5.2 and 6.0. Notably absent are the intermediate versions, so this is a list of minimum supported patch releases rather than a range. The note also says there are no major changes in the API and most users will do not need to change anything in their code, which is a claim worth checking against your own usage since the removal of old Python and Django support can still break an environment that was relying on transitive resolution.

Two features in that 6.0 note changed how the library is used in templates rather than in models. One adds a way to specify thumbnail format, contributed by autoantwort. The other makes it possible to generate srcset with the thumbnail template tag, also from autoantwort, which is a responsive images feature rather than a cosmetic one. A third contribution from anentropic adds an option to suppress exceptions with templatetags, and that one is a behavioural switch worth knowing about because it changes a template rendering failure into a silent missing image.

The 6.1 release from 22 February 2026 is smaller. It tests Django 6, and it adds a request-local state cache to reduce cache round trips from mgalgs, recorded in the notes as that contributor's first contribution. The idea is that a single request generating several thumbnails should not repeat the same cache lookup for each one. Then 6.1.1, on 15 September 2026, is a single fix, Django 6.1 support.

The last commit on the repository is dated 15 September 2026, the same day as the 6.1.1 tag, so the default branch and the newest release are in step. That is worth saying plainly because it is the opposite of the ReaLTaiizor pattern, where a develop branch runs years past the last tag.

## What to check before you depend on it

Four things, in order.

First, read the RTD documentation rather than the README. The README in this repository is reStructuredText and it ends partway through an example, which means the sections covering cache configuration, regeneration behaviour and the full processor list are not in the file most people read. The homepage the repository advertises is django-imagekit.rtfd.org, and the README links to the same documentation under a slightly different host name, django-imagekit.readthedocs.org. Both resolve to the RTD project, so that is a naming inconsistency rather than a broken link, but it is a sign that the README was written at different times than the docs.

Second, decide between the two field types before you write the model, because the choice determines whether adding a thumbnail later costs you a migration and a backfill. ImageSpecField adds nothing to the database, which means existing rows get a working thumbnail the moment you deploy, and also means you have no way to query on the derived path. ProcessedImageField stores the path, which means you can query it, and means every row needs a migration and, depending on your setup, a generation pass for existing data.

Third, check where the cache directory lands relative to your media serving. The content addressed path in the README example is /media/CACHE/images/ followed by a hash and an extension. If you serve media through a separate domain, a CDN, or a non filesystem storage backend, confirm that the CACHE subdirectory follows the same route, because ImageKit will not warn you, it will simply write somewhere the URL cannot reach.

Fourth, read the open issue list. With 91 open issues on 2,347 stars, that is a real queue, and for a library this old and this embedded in other people's code, a meaningful share of those are probably the processor and format behaviour edge cases you are about to hit. The developer workflow for the project itself is conventional, a justfile with a publish target that runs a build and a twine upload, a test target that shells out to tox, plus tox.ini and a coverage config, so contributing is a normal fork and pull request rather than anything bespoke.

## Conclusion

django-imagekit earns its place in a Django project because it removes the part that everybody reinvents badly, which is deciding when a derived image is generated, where it is stored and how a template gets a URL for it. The concept is small enough to hold in your head, an image spec is a type of image generator that generates a new image from a source image, and the storage layout is a content hash under a CACHE directory, which means the same input and the same spec produce the same filename. The things to weigh before adopting are concrete. The README is reStructuredText and ends mid-example at 7,000 characters, so the caching and regeneration behaviour lives in the RTD docs rather than in the file most people read. The repository description says Currently v6.0 while the newest tag is 6.1.1 from 15 September 2026, so the one line of metadata that search engines show is behind the package index. And with 91 open issues on 2,347 stars, the queue is large enough that a project depending on a specific processor behaviour should read the open list before assuming a bug is their own code.

## FAQ

### How does ImageKit work?

You declare an image spec, which the documentation defines as a type of image generator that generates a new image from a source image. The spec lists processors such as ResizeToFill along with an output format and encoder options. When you access the spec's url, ImageKit applies the processors and returns a path under a content addressed cache directory, for example /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg, so the same source and the same spec always produce the same file with no database lookup.

### What does an ImageSpecField add to my Django database?

Nothing. The README's own note describes ImageSpecFields as virtual, adding no fields to your database and not requiring a database, so adding one produces no migration and no column. The trade is that the file path must be computed from the source image and the spec. If you want a stored path and are willing to run a migration, use ProcessedImageField instead, which the README says behaves exactly like a Django ImageField and saves the file path in the database.

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

The 6.0 release notes, dated 12 October 2025, list supported Python versions as 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14, and supported Django versions as 3.2, 4.2, 5.2 and 6.0. The pyproject.toml in the repository agrees on the Python floor with a requires-python of at least 3.9. Support for Django 6.1 was added afterwards in release 6.1.1 on 15 September 2026.

### Why does the repository say v6.0 when the newest release is newer?

The repository description field is stale. It reads Currently v6.0, while the tags show 6.1 from 22 February 2026 and 6.1.1 from 15 September 2026. The commit history is not behind, the last commit is the same day as the 6.1.1 tag, so only the one line of metadata is out of date. Pin versions from your package manager rather than from the repository description.

## Sources

- [License: BSD-3-Clause](https://github.com/matthewwithanm/django-imagekit/blob/develop/LICENSE)
- [matthewwithanm/django-imagekit on GitHub](https://github.com/matthewwithanm/django-imagekit)
- [Project website](http://django-imagekit.rtfd.org/)
- [README](https://github.com/matthewwithanm/django-imagekit/blob/develop/README.md)
- [Releases](https://github.com/matthewwithanm/django-imagekit/releases)

---

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