A minimalist illustration against a teal background shows a white keyhole rising from what appears to be a diagonal surface or slope. The clean, geometric design suggests unlocking solutions, breakthrough moments, or finding clarity after a long wait - fitting imagery for Django's 19-year journey to implement composite primary keys.

"This is likely to be very difficult."

With these six words in 2005, Django co-creator Jacob Kaplan-Moss opened what would become one of the framework's longest-running feature requests. The ticket's title was deceptively simple: "Add support for multi-column primary keys." Its implications would echo for nearly two decades.

In the world of web development, where frameworks and libraries seem to emerge weekly, it's rare to find a feature request that predates Twitter or even the iPhone. Yet here we are in 2024, celebrating the addition of composite primary keys to Django 5.2 – a capability first requested when Netflix was still mailing DVDs.

Understanding Composite Primary Keys

If you've worked with databases for any length of time, you've likely encountered situations where a single field just isn't enough to uniquely identify a record. Think about a university's course schedule: a course number alone isn't unique – it's the combination of the course number, semester, and year that tells the full story. Or consider international flight records, where the flight number is only unique when combined with the date and airline code.

This is where composite primary keys come in. They're the database's way of saying, "All of this information together uniquely identifies this record." This concept is as old as relational databases, and every major database engine, from PostgreSQL to MySQL, supports it.

Yet, Django developers have had to work around this limitation and the common solutions weren't always elegant:

  • Implementing unique_together constraints (which helped with uniqueness but didn't solve the primary key problem)
  • Building custom model fields (a solution that often felt more like a hack than a feature)

The new CompositePrimaryKey field changes all of this. Here's a glimpse of what it will look like in Django 5.2:

from django.db import models

class CourseOffering(models.Model):
    pk = models.CompositePrimaryKey("course_number", "semester", "year")
    course_number = models.CharField(max_length=10)
    semester = models.CharField(max_length=10)
    year = models.IntegerField()

Understanding the Technical Challenge

The Long Road to Implementation

"This is something that gets asked about a lot," wrote Jacob Kaplan-Moss in 2006. That simple statement would kick off an 18-year journey of discussions, proposals, and technical challenges that perfectly illustrates the complexity of maintaining a widely-used framework.

Kaplan-Moss articulated the initial challenges well:

  1. The obj._meta.pk API was deeply woven into Django's fabric. Any solution would need to maintain compatibility with countless codebases depending on this interface.
  2. Core features like the comment framework and admin logging relied on simple (content_type_id, object_pk) tuples. How could these be evolved without breaking existing sites?
  3. Admin URLs, following Django's clean URL patterns (/app_label/module_name/pk/), would need rethinking. How do you represent multiple fields in a URL while maintaining Django's commitment to elegance?

Far from simple implementation details, these challenges forced Django's maintainers to confront foundational decisions already made and the status quo of 19 years.

The Community Speaks

The comments on the issue tracker tell a story of persistence. "Lack of support for composite primary keys is a major drawback of Django and should be dealt with as a priority," wrote one developer in 2010. "Most applications use databases with tables having composite primary keys. Such applications cannot be migrated to Django which reduces Django's usability."

However, in open source, knowing something needs to be done isn't the same as having the resources to do it. For years, the discussion evolved alongside Django itself. Each major release brought new considerations: How would this interact with migrations? What about new database backends? How could it work with Django's growing REST framework ecosystem?

Solving the Original Challenges

Remember those three critical issues Jacob Kaplan-Moss outlined back in 2006? Let's look at how Csirmaz's implementation elegantly addresses each one:

1. The obj._meta.pk API Challenge

def contribute_to_class(cls, name, private_only=False):
    super().contribute_to_class(cls, name, private_only=private_only)
    cls._meta.pk = self
    setattr(cls, self.attname, self.descriptor_class(self))

This solution maintains compatibility with Django's existing patterns while introducing the new functionality. The CompositePrimaryKey field seamlessly integrates with Django's metadata system, ensuring that existing codebases continue to work while enabling the new composite key functionality.

2. The ContentType Tuple Problem

class CompositeAttribute:
    def __get__(self, instance, cls=None):
        return tuple(getattr(instance, attname) for attname in self.attnames)

    def __set__(self, instance, values):
        attnames = self.attnames
        length = len(attnames)

By implementing the composite key as a tuple under the hood, the solution maintains compatibility with Django's existing tuple-based lookups while providing a clean API for developers. The implementation handles both reading and writing composite keys in a way that feels natural to Python developers.

3. The Admin URL Challenge
The implementation includes specialized lookups for composite keys:

CompositePrimaryKey.register_lookup(TupleExact)
CompositePrimaryKey.register_lookup(TupleGreaterThan)
CompositePrimaryKey.register_lookup(TupleIn)

These registered lookups enable Django's URL routing system to handle composite keys elegantly, solving the admin URL challenge while maintaining Django's clean URL patterns.

What makes this implementation particularly brilliant is how it builds on Django's existing patterns rather than fighting against them. Take the validation system:

def _check_field_name(self):
    if self.name == "pk":
        return []
    return [
        checks.Error(
            "'CompositePrimaryKey' must be named 'pk'.",
            obj=self,
            id="fields.E013",
        )
    ]

By enforcing that composite primary keys must be named 'pk', it maintains Django's conventions while extending its capabilities. It's exactly the kind of solution that feels "Django-ish" – sensible defaults, clear constraints, and developer-friendly APIs.

Open-Source Plays the Long Game

In "Working in Public," Nadia Eghbal captures a truth about the composite primary keys story perfectly: "The bigger your project becomes, the harder it is to keep the innovation you had in the beginning... Suddenly you have to consider hundreds of different use-cases..."

This tension – between innovation and stability, between feature requests and maintenance – played out across 19 years of discussion. Each proposed solution had to contend not just with technical challenges, but with the weight of Django's success: thousands of existing projects, countless custom implementations, and an ecosystem that had grown up around the framework's established patterns.

The breakthrough finally came through Google Summer of Code 2024, where fresh eyes could examine old problems. Under the mentorship of Lily Foote, Bendegúz Csirmaz brought a new perspective to the challenge. His proposal didn't just offer technical solutions; it demonstrated an understanding of why this feature had remained elusive for so long. His approach acknowledged both the technical debt of the past and the distributed, multi-tenant future of web applications.

When the pull request finally landed, fellow developer David Halter captured the community's sentiment perfectly: "The amount of work and persistence that @csirmazbendeguz put into this is awesome. Thanks also to the reviewers for your patient and encouraging work! It is a reassuring feeling, that such big and useful changes can still land within Django, without sacrificing quality."

GitHub comment from davidhalter celebrating the successful merge of Django's composite primary keys PR, praising @csirmazbendeguz's persistence and the reviewers' patient work. The comment has received several emoji reactions including hearts and rocket ships.

That last line particularly resonates: "without sacrificing quality." In an era where software often prioritizes speed over stability, Django's commitment to thoughtful, well-reviewed implementations stands out. It reminds us that in open source, the goal isn't just to ship features—it's to ship features that will stand the test of time.

"In database design, composite primary keys are often necessary for the partitioning and sharding of database tables," Csirmaz wrote in his proposal. This wasn't just about solving a long-standing problem – it was about positioning Django for the next decade of web development.

What makes this resolution particularly elegant is how it maintains Django's commitment to developer ergonomics while solving a complex architectural challenge:

class User(models.Model):
    pk = models.CompositePrimaryKey("tenant_id", "id")
    tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
    id = models.IntegerField()

The Road Ahead

When Django 5.2 ships with composite primary key support, it won't just be adding a feature – it'll be closing a chapter in open-source history. This 19-year journey, from Jacob Kaplan-Moss's initial "this is likely to be very difficult" to Bendegúz Csirmaz's GSoC implementation, reminds us that open source development isn't just about code – it's about community, persistence, and the power of fresh perspectives.

The resolution through Google Summer of Code feels particularly fitting. A program designed to attract new contributors to open source is becoming the catalyst for solving one of Django's longest-standing challenges. That's the kind of full-circle moment that makes open-source development special.

As we look forward to Django's future, this milestone reminds us that great software isn't just about code—it's about community, patience, and the long-term vision required to build tools that stand the test of time. As longtime Django developers ourselves (it's why we love building Django apps for our clients), we've seen firsthand how this commitment to quality over quick fixes has made Django the reliable framework it is today. Sometimes, the best features are worth waiting for—and sometimes, they're worth waiting 19 years for.

Related Posts

software partner
February 15, 2020 • Micah Widen

7 Strategies For Choosing A Custom Software Partner

Over the next eight years, the employment of software developers is expected to grow by 21%. With this overwhelming supply of experts, how do you choose the right software provider?

Demonstration of GDPR compliant data security practices using handheld device.
June 7, 2018 • Nick Farrell

Techniques for Personal Data Privacy

Over the past decade, we’ve seen what can happen when companies are neglectful with personal data, and in 2018, strong privacy practices can ensure that your company is making headlines for the right reasons.