DEV Community

rodbv
rodbv

Posted on

Djangonaut diaries, week 6: Charting the Unknown Unknowns of a PR

Hi there!

This week's post is less technical, a reflection of the journey so far, while I celebrate my first PR merged and wait for two more to be reviewed.

More specifically, it's reflection on the PR for ticket #10919, which is in progress.

My first Django core PR

At my $DAYJOB, we live and breathe the "Build, Measure, Learn" cycle. Our lead time from "ready for dev" to "deployed" in the Kanban board is measured in a few days, and we routinely deploy a few dozen times a week under CI/CD.

We rely heavily on feature flags to mitigate risk; if something doesn't pan out or a weird bug slips through, we simply flip a toggle, check the error logs, and iterate. Worst case (which is rare), we can rollback the deployment with a few clicks. It’s a nimble, high-velocity speedboat where the cost of failure is relatively low.

But stepping into Django core to tackle #10919 is an entirely different reality, and it's been a great and rewarding learning experience.

This issue opened back in 2009, regarding large browser memory usage and potentially inaccessible pages generated when deleting highly nested objects in Django Admin. See that red button all the way to the bottom of the page? there's where the "delete" button ends up when deleting objects with a lot of nested children.

Delete object page in Django Admin with a huge list and a red delete button at the bottom, 50k pixels down.

Across the globe on a cargo ship

Contributing to Django isn't steering a speedboat; it is helping load and route a massive cargo ship.

Let me be clear: by comparing Django to a cargo ship, I am not saying it's slow or heavy. Django ships a new version every eight months like clockwork, constantly delivering a ton of great, innovative features while keeping very solid long-term support via its LTS program.

But its speed is measured in steady throughput and predictability, not rapid-fire pivots. It is a cargo ship because it delivers a massive amount of value across vast distances. Hundreds of thousands of businesses around the world depend on that cargo arriving safely. And when you are building for a ship this size, you quickly need to learn how to deal with its own momentum.

The Illusion of 98% Done

When I opened the PR in mid-March, I honestly thought the hardest part was behind me. I had an elegant solution: to prevent forcing an unexpected UI change on legacy projects, I proposed an opt-in, configurable attribute (ModelAdmin.delete_confirmation_max_display).

The interactions with the Django Fellows reviewing the code were fantastic. They asked incredibly well-considered questions that sharpened the implementation and deepened my understanding of the framework's constraints. They taught me the importance of well-crafted commits and smart use of rebase, to help them review new iterations and bissect any regressions.

I suddenly had to navigate the world of Sphinx and ReStructuredText (rst files). Where exactly does a new Admin setting belong in the massive official documentation? How do you format a .. versionadded:: 6.1 directive? You aren't just writing a quick explainer for your team; you are writing the manual that a ton of developers (and now, AI) is going to rely on.

After a few force pushes and refinements to keep the review process moving, the tests were green, all requests were considered, PR was marked as accepted! I felt like I was 98% done, merge was a few days away.

The Bucket of Cold Water (And Why We Need It)

By late March, the code was solid, but we needed to settle a specific UI detail: when truncating the list of deleted objects, should we cap the items globally across the whole page, or per nesting level? Better open up this decision to a larger audience, and following a suggestion by the reviewer, I headed over to the Django Forum, expecting a straightforward vote.

Instead, the very first response I received was a complete bucket of cold water: why not scrap the PR entirely and close this 17-year-old ticket?

The commenter argued that large-scale applications should just handle this themselves by overwriting some hooks, which is a totally feasible thing. From their perspective, adding this extra logic to the core was an unnecessary complication.

When your daily rhythm involves a handful of deployments and PRs merged, and the constant reward of shipping quickly, getting told to throw all your work away stings. This PR was my pet project for a month!

But once the initial sting faded, I realized that this pushback wasn't just necessary friction; it was incredibly positive. Why?

In a massive framework like Django, every new feature, even a useful one, is a liability. It adds complexity and carries a maintenance burden that the community will have to support for years. The most vital stress-test in foundational open source isn't asking, "Is this useful?" It's asking, "Should we REALLY implement this?" Being deliberately sparse with new additions and ruthlessly questioning their necessity is the exact reason Django can continue shipping those reliable 8-month releases without collapsing under its own weight, for over 20 years.

The beauty of the open-source community is the sheer diversity of thought that this stress-test provokes. While I was still digesting that bucket of cold water and practicing some stoicism, a Django Fellow chimed in with the exact opposite view. Looking at the accessibility issues of hiding the "Delete" (actually, "Yes, I'm sure") button beneath thousands of rows, they saw great value. They argued that not only should the feature exist, but it might even need to be the default behavior out of the box!

Slow Coding and the Antidote to AI Fatigue

I was suddenly standing between "throw this away" and "maybe we want to make this the default behavior, not opt-in".

I didn't want to go "defend" my position too fast (in fact, I haven't replied yet). I see the value of letting the discussion breathe, and ultimately, see where we find a consensus. In the meantime I decided to keep it an opt-in, configurable attribute, but use a global limit for a cleaner UI (it's much better indeed!).

This meant I had to completely rewrite the core logic.

Instead of a simple per-level cap, I needed to revisit a recursive function with some weird data structure and maintain a global counter across deeply nested tree of deleted objects.

At my $DAYJOB, to keep up with our velocity, I rely heavily on AI assistants. I can’t exactly tell my client, "Hey, I’m going to turn off my AI support for a few days just to exercise my brain, okay?"

But here, on a PR that might take days or weeks between checks? I had the ultimate luxury: time. This deliberate pace turned out to be the perfect antidote to AI fatigue.

Instead of prompting an LLM to rewrite the tree traversal, I spent a few days just turning the options over in my head while doing other things. For anyone who genuinely enjoys algorithms, stepping away from the prompt box and wrestling with the logic mentally is fun. At least for me.

In early April, I finalized the global counter logic, pushed the new commit, and hit 100% test coverage again.

The Rhythm of the Framework

As of writing this, the PR is still open, and that is exactly how it should be.

Right now, the Django Fellows are focused on the massive task of wrapping up the Django 6.1 release. A 17-year-old ticket can happily sit a little longer while the next major version of the project gets the focused stabilization it needs.

The reward of this process wasn't about getting a "Merged" badge on GitHub as fast as possible. The true value was uncovering those unknown unknowns...the rigor of the docs, the depth of the edge cases, the craftsmanship of commits, the importance community consensus, and the joy of manual algorithmic thinking. While I wait, I’m using this time to dive into other Trac issues and check pull requests from my fellow Djangonauts.

It’s just more time to learn the ropes! (and enough with nautical analogies now!)

See you!

Top comments (0)