Every developer’s open-source origin story usually starts with the same comforting delusion: “I’ll just pick up a quick documentation ticket, fix a typo, and collect my shiny contributor badge before lunch.”
But when I joined Session 7 of Djangonaut Space and got placed in Team Carina, I quickly realised that contributing to a framework as battle-tested as Django doesn’t work like that, and honestly, I’m glad it doesn’t.
In Djangonaut Space, you aren’t just thrown into the deep end of Trac and told to swim. Having our Navigator, Lilian, and our Captain, Maryam, guiding Team Carina gave me a real framework for how to approach open source like an engineer rather than a drive-by code committer: understand the problem at its root before you even think about claiming a ticket.
Reproducing a 14-Year-Old Misconception.
While hunting through Django’s Trac issue tracker, I came across Ticket #18543 (a ticket opened 14 years ago) regarding ImageField validation.
Spoiler alert: it was among the tickets curated by my Navigator.
The core complaint on the ticket was deceptively simple: the documentation for django.db.models.ImageField originally claimed that the field "validates that the uploaded object is a valid image." At some point over the years, someone tried to address the confusion by adding a note to the documentation:
Django does not validate whether an uploaded file is an image.
We recommend to validate image file correctness in forms, or serializers.
Wait, does it validate images, or doesn’t it?
Instead of blindly clicking “Assign to me” on Trac and editing text, I wanted to see what Django was actually doing under the hood. If a ticket has survived for 14 years, there is a reason people are confused by it.
I updated my forked Django repository, spun up the local test environment, and wrote a small regression test to reproduce the behaviour firsthand. I created a model with an ImageField, passed it a corrupted non-image byte stream (essentially a text file pretending to be an image), and ran full_clean() directly on the model instance.
It passed without raising a ValidationError.
Only after seeing that test confirm the exact behaviour described in the ticket did I go back to Trac, officially claim #18543, and start digging into django/db/models/fields/files.py to understand why.
Where Django Actually Validates Files.
Tracing the source code revealed why that documentation paragraph had been tripping developers up for over a decade. Django splits ImageField across two completely different architectural layers that happen to share the same name:
django.db.models.ImageField (The Model Layer): This class inherits directly from models.FileField. If you inspect its methods in django/db/models/fields/files.py, it never overrides validate() or clean(). In fact, the model layer only ever touches Pillow lazily inside update_dimension_fields()—and only if you explicitly set width_field or height_field on your model. Otherwise, calling model.full_clean() performs zero image verification beyond basic FileField checks (like max_length or nullability).
django.forms.ImageField (The Form Layer): When a ModelForm or the Django Admin renders a model with an ImageField, the model field’s formfield() method swaps in django.forms.ImageField as the default form class. That class hooks into Pillow inside to_python() to verify that the uploaded file is a genuine, non-corrupted image.
The existing documentation tried to warn developers about the model layer’s limitation, but by broadly declaring “Django does not validate whether an uploaded file is an image. We recommend to validate image file correctness in forms or serializers,” it accidentally misled developers into thinking they had to write custom validation logic from scratch, even when using standard Django forms!
Opening PR #22210 and the Lesson of EAFP.
With the reproduction confirmed and the codebase traced, I opened PR #22210. When you are making your very first pull request to Django Core, your natural instinct is LBYL (Look Before You Leap): you hesitate in the comments, overthink every word, and want to ask permission before making a move so you don’t get it wrong in front of seasoned maintainers.
Early in the review process, Johannes Maron dropped a piece of Python wisdom that completely changed how I view open-source collaboration: EAFP (Easier to Ask for Forgiveness than Permission). Instead of waiting around asking if an approach is okay in theory, push the commit. Give reviewers a concrete diff to react to, and trust the review pipeline to refine it.
Architectural Boundary in Technical Documentation.
What followed on PR #22210 was a masterclass in technical writing and software architecture. In Django, PRs go through rigorous community review before being merged by one of the Django Fellows. Watching Johannes Maron, Tim Schilling, and Django Merger Sarah Boyce weigh in on my diff taught me that good technical documentation is never “just text”, but instead it is a strict reflection of architectural boundaries.
Takeaways for New Contributors.
Landing my first commit in Django core left me with a few practical principles for future tickets:
Read the implementation, not just the docs: Even on a documentation ticket, accurate technical writing comes from tracing the underlying Python code (validate(), clean(), and formfield()).
Practice EAFP on pull requests: Push your best attempt early. Reviewers can refine a concrete diff much faster than a theoretical question.
Respect architectural boundaries in documentation: Good reference docs mirror the separation of concerns in the codebase itself. Keeping model-layer docs focused on model behavior prevents confusion down the line.
Give reviewers room to align: When multiple reviewers are weighing trade-offs on your PR, pay close attention to the codebase conventions they cite, and be ready to synthesize their feedback when asked.
Thanks to Maryam(Captain), Lilian(Navigator), Tim Schilling, Johannes Maron, and Sarah Boyce for the thoughtful reviews on PR #22210, and to the Djangonaut Space organizers and my Team Carina peers for the constant support. On to the next ticket!
Top comments (0)