April 4, 2017·5 min read·engineeringpythondjangocompatibility

Django 1.11 gives Python 2 applications a supported transition

Django 1.11 combines long-term support and Python 3.6 compatibility with explicit subqueries, index classes, and template-based form widgets.

A framework can support an old runtime while giving applications a way to leave it. Django 1.11 occupies that position. It is the last Django series supporting Python 2, and it is also the first to support Python 3.6. Its long-term support designation gives existing applications time to separate a framework upgrade from the work of changing Python versions.

The April 4 release announcement promises security and data-loss fixes for at least three years. That promise has a defined scope. Fixes for crashing bugs, significant bugs in newly introduced functionality, and regressions are scheduled through December 2017. Long-term support does not mean that every category of maintenance continues unchanged for the entire period.

I choose Python largely because its ecosystem already supplies implementations of difficult application concerns. A framework with a stated support policy makes that choice easier to evaluate. The runtime transition still requires work, but the policy identifies which framework version can support that work without requiring an immediate move to the next major series.

Compatibility is a release property

The release notes shipped with Django 1.11 list Python 2.7, 3.4, 3.5, and 3.6 as supported versions. They recommend and officially support the latest release within each series. Supporting a Python series therefore does not imply a promise to preserve behavior on every older patch release in it.

Those notes also state the plan for Django 2.0 to require Python 3.5 or newer. As of this release, that is the announced direction for the next major version. Django 1.11 provides an overlap in which an application can use the same framework series on either side of the Python 2 transition. Dependencies outside Django still need their own compatible versions, so the overlap solves one important constraint without deciding every dependency question.

Library authors have a related problem. Django 1.11 stops displaying its own deprecation warnings by default, matching Python's default handling. The notes explain that this allows third-party applications to support both the 1.8 and 1.11 LTS series without adding code solely to avoid warning output. Warnings remain useful diagnostic information, but their default visibility is a separate policy from the continued availability of the deprecated API.

This is relevant when a supported application emits no warnings during ordinary execution. Silence does not establish that every API it uses will survive the next major release. Django's notes describe enabling warnings when checking compatibility. The framework's support window and its deprecation process describe different obligations, even though both affect the timing of an upgrade.

Subqueries become explicit ORM expressions

The release adds Subquery, Exists, and OuterRef. The first two represent explicit subqueries, while OuterRef allows the inner query to refer to fields from the outer query. This extends the ORM's vocabulary for expressing relationships between queries without requiring the application to leave that vocabulary merely to name a correlated lookup.

The useful distinction is between constructing a query and executing repeated queries in application code. An outer reference expresses a dependency inside the database query. It does not ask Python to retrieve each outer object and issue a separate lookup itself. The database still needs to execute the resulting SQL, and the new expression classes do not make all query shapes equivalent in cost.

The API matters for maintainability because the relationship remains visible alongside the rest of a queryset expression. A developer can see that the inner selection depends on the current outer row. It also preserves the need to understand SQL semantics. Wrapping a subquery in a Python object does not remove questions about the number of rows or columns that the surrounding expression expects.

Indexes have their own classes

Django 1.11 introduces index classes and the Meta.indexes option. The base Index creates a B-tree index, and the release notes compare it with existing field-level indexing and index_together. An index can also specify ascending or descending order for its columns. The class structure permits database-specific index types through subclasses.

For a model containing a name field, a simple declaration has the following form. The surrounding model can continue to define its fields normally.

from django.db import models

class Customer(models.Model):
    name = models.CharField(max_length=100)

    class Meta:
        indexes = [models.Index(fields=["name"])]

The release includes PostgreSQL GinIndex and BrinIndex classes as well. These are concrete database choices, so the index abstraction does not pretend that every backend has the same indexing capabilities. Keeping the declaration in the model gives the index an explicit representation in application schema code. Choosing the appropriate index still depends on the database operations the application needs.

This fits Python's strength in extensible object protocols. The common interface can represent an ordinary index while a subclass carries a database-specific meaning. I find that arrangement more useful than an abstraction that obscures the database distinction and leaves applications to rediscover it later.

Widget rendering moves into templates

Form widgets now render through the template system instead of building their output in Python. The release announcement identifies easier widget customization as a main benefit. That puts markup changes in the same kind of material developers already edit for the rest of an application's presentation.

The change also removes some older customization mechanisms. The shipped notes remove Widget.format_output() and direct users toward custom widget templates. They list removed undocumented widget classes and add a renderer argument to Widget.render(). Existing custom widgets therefore need attention at the point where they extend the previous implementation.

This is a meaningful compatibility tradeoff within an LTS release. Long-term support begins with a particular release's behavior, including its documented changes. It does not promise that private hooks or removed methods from older releases continue to work. The new rendering mechanism offers a supported place to express customization, while application-specific extensions must match that mechanism.

The release changes the status of older Django series too. The announcement places Django 1.10 into security and data-loss maintenance through December and ends extended support for Django 1.9. Those dates affect the choice of a maintained framework version independently of whether an application needs subqueries or new widgets. Django 1.11's practical role is to combine that maintained baseline with explicit Python-version overlap, so applications can perform the runtime transition while remaining within one supported framework series.