Section 1: The Bug That Sat Untouched for Over a Year
In September 2026, I merged my first Pull Request into Django REST Framework — one of the most widely used Python libraries in the world.
But the story didn't start with me. It started with a bug that was reported in May 2025 and sat untouched for over a year.
The bug was subtle but serious: DRF's serializer validation was rejecting perfectly valid data when using Django's UniqueConstraint with conditions.
Here's what that means in practice. Imagine you're building an API for a race tracking system. You want to enforce a simple rule:
"Only one race with a given name can have a position ≤ 1."
So you write this constraint:
UniqueConstraint(
fields=['race_name'],
condition=Q(position__lte=1),
name='unique_top_race_name'
)
The database handles this correctly. It allows:
- ✅
Marathon, position=1 - ✅
Marathon, position=2 - ✅
Marathon, position=3
But DRF's serializer? It would incorrectly reject the third entry with a "unique set" error — even though the database would happily accept it.
The user experience was broken. Developers were forced to bypass the serializer and insert data directly into the database, which is exactly the kind of thing DRF exists to prevent.
The worst part? The issue was marked as "confusing" by one of the maintainers themselves. It involved complex interactions between Q objects, referenced_base_fields, nulls_distinct, and two competing validator classes.
Nobody wanted to touch it.
That's exactly why I did.
Section 2: The Journey — Reviving an Abandoned PR, Surviving Three Merge Conflicts, and Facing Copilot
Once I decided to tackle this bug, I discovered that someone had already started working on it — a developer named nefrob had opened a Pull Request months earlier, but it had stalled.
The PR had:
- ❌ Merge conflicts with the main branch
- ❌ Missing helper functions it depended on
- ❌ Unresolved reviewer feedback
- ❌ No activity for weeks
Most people would have walked away. Instead, I decided to revive it.
What I actually did
1. Rebased and resolved the initial conflicts.
I pulled the abandoned branch, resolved the merge conflicts, and added a missing helper function (get_referenced_base_fields_from_q) that the original author had referenced but never committed. Without it, the whole PR wouldn't even import.
2. Faced three rounds of new merge conflicts.
Every time a maintainer merged something new into main, my branch would break again. I resolved conflicts three separate times — each one requiring me to carefully combine my logic with changes from other contributors who were working on the same file.
3. Fixed four bugs found by GitHub Copilot.
Copilot reviewed my PR and found issues I had missed:
-
Empty
constraint.fields— my code was creating validators with no fields, silently breaking validation. - Lost custom error messages — when multiple constraints shared the same fields, only the last one's message survived.
- Condition fields not triggering revalidation — updates that only changed the condition field were silently accepted, even when they violated the database constraint.
-
A
nulls_distinctedge case — partial updates were incorrectly skipping validation.
Each bug required careful investigation. Each one needed a test to prove the fix.
4. Got reviewed by a contributor who'd worked on the same code.
A contributor named MehrazRumman — who had recently merged his own PR touching the same file — reviewed my work in detail. He found two more issues:
- A dead-code helper that could be removed entirely (since DRF only supports Django 5.2+, which already provides
Q.referenced_base_fields). - A performance regression where partial updates triggered three unnecessary database queries instead of zero.
I applied both fixes. That meant deleting 20 lines of code I'd originally added — and that was the right call.
5. Passed the final review and got merged.
After 17 commits, 26 comments, and more than a month of back-and-forth, the PR was approved and merged by a DRF maintainer.
The result
The final change:
- ✅ Fixes incorrect validation for conditional
UniqueConstraints - ✅ Preserves custom error messages and error codes
- ✅ Correctly rechecks uniqueness when condition fields change
- ✅ Handles
nulls_distinctedge cases - ✅ Passes all 83 validator tests
- ✅ Merged into Django REST Framework
A bug that had been open for over a year — and that one maintainer openly called "confusing" — was finally closed.
Section 3: Five Lessons I Took Away (And Why They Matter Beyond Open Source)
Merging a PR into Django REST Framework taught me more than any tutorial could. Here are the five lessons that stuck with me — and that apply far beyond open source.
1. Reviving abandoned work is more valuable than starting from scratch
The original PR had been sitting untouched for months. Instead of writing my own solution from zero, I picked up where someone else left off — resolved the conflicts, filled in the gaps, and carried it across the finish line.
The lesson: In any team, the unfinished work of others is often the highest-leverage place to contribute. Don't ignore the graveyard of "almost done" projects.
2. Automated tools find what humans miss — but they don't replace judgment
GitHub Copilot found four bugs in my code that I had completely overlooked. Each one was subtle, each one was real, and each one would have caused problems in production.
But Copilot also made suggestions I chose not to follow — and I had to justify why. Automation is a force multiplier, not a decision-maker.
The lesson: Use AI tools aggressively to review your work, but always own the final call.
3. The best code is often less code
A reviewer pointed out that one of my helper functions was dead code — a compatibility shim for Django versions the project no longer supported. I deleted 20 lines I'd been proud of writing.
That deletion was the single best thing I did in the entire PR.
The lesson: Code you don't write is code you don't have to maintain, test, or debug. Simplicity is a feature.
4. Merge conflicts are a sign of a healthy project — not a broken one
I resolved conflicts three separate times during this PR. Every time, it meant other contributors were actively improving the same code I was touching.
A project with zero merge conflicts is usually a project with zero activity.
The lesson: Friction from collaboration is not a bug. It's the cost of working on something that matters.
5. Patience is a technical skill
From my first comment to the final merge, this took over a month. There were days of waiting for reviewers, weeks of silence from maintainers, and moments when I wondered if it was worth continuing.
It was. The maintainers are volunteers. The reviewers have day jobs. And the bug wasn't urgent — it had waited a year already. It could wait one more week.
The lesson: In open source — and in any long-running project — patience isn't passive. It's an active skill you have to practice.
Final thought
I didn't contribute to Django REST Framework because I'm an expert. I did it because I was willing to:
- Read the issue carefully
- Pick up someone else's unfinished work
- Accept feedback without taking it personally
- Delete my own code when it wasn't needed
- Wait when waiting was the right move
A year-old bug that one maintainer called "confusing" is now closed. And a small piece of my work is running in thousands of production APIs around the world.
If you've been waiting for the "right moment" to contribute to open source — this is it.
Pick an issue. Start small. Be patient. Ship it.
About the author:
Majid Khazaei is a software engineer and open-source contributor.
He recently contributed to Django REST Framework, fixing a validation bug
that had been open for over a year (PR #10021).
🔗 GitHub: https://github.com/majidkhazaei
🔗 LinkedIn: https://www.linkedin.com/in/majid-khazaei-dev
Top comments (2)
The line about deleting your own code is the most transferable one here. A helper that exists to support versions the project has already dropped is worse than dead code: it looks load-bearing, so the next contributor has to reason about it before discovering it can go. Reviewers who ask "why is this here" and accept "let me remove it and see what breaks" are cheaper than any amount of documentation.
The catch the second reviewer found — partial updates firing three queries where zero were needed — is a good argument for putting round-trip count in the review checklist for serializer and validator changes rather than in benchmarks. A suite that passes either way will never surface it; it surfaces in the p99 of an endpoint nobody is measuring.
Picking up a stalled PR instead of opening a clean one is also underrated. The original author's tests and the issue discussion are usually the expensive part, and a fresh implementation throws them away to avoid reading them.
Thanks - you've read this more carefully than I wrote it. Three distinctions
I didn't make clearly, worth unpacking:
On dead code: you've named something I couldn't articulate at the time. The
helper wasn't just unused - it looked necessary. It had a docstring, a
fallback branch, and it was imported in two files. A reader would assume
removing it would break something. The only way to be sure was to check
pyproject.toml and confirm the project had already dropped every version that
fallback served. So it's not just dead code - it's misleading dead code,
which costs more cognitive load than an obviously empty function.
On query counts: "surfaces in the p99 of an endpoint nobody is measuring" is
exactly right. My test suite passed both before and after the initial version
of the fix. The regression only became visible when a reviewer ran a profiler.
Since then, I've started adding query-count assertions to my own test suites
for any code that touches the ORM - not as a performance test, but as a
correctness test. If a lookup that should short-circuit goes to the database,
that's a bug, not just a slow path.
On reviving stalled PRs: the "tests are the expensive part" insight deserves
more attention than I gave it. When I picked up nefrob's branch, I inherited
his test cases, his comments, and the reasoning behind the original design.
Writing that from scratch would have taken days. I mostly just had to update
them to match the new behavior.
The one nuance I'd add: you also inherit the original author's constraints.
There were decisions nefrob made months earlier that I had to decide whether
to preserve or revise based on new information. That's a different kind of
judgment call than starting fresh.