On October 19, the authors of PEP 836 withdrew it, and with that the experimental JIT compiler is coming out of CPython's main branch. If you skimmed the headlines, it sounded like Python just gave up on being fast.

I build and maintain Django backends for a living, and my honest reaction was: for the apps I work on, this changes almost nothing, and I think it's the right call. Here's what actually happened, why a web app was never going to feel the JIT much, and what I'd measure instead if you want your Django app to be faster this quarter.

What actually happened

Sticking to what the PEP and the reporting around it say:

  • The JIT has been in CPython since 3.13, experimental and off by default. The official Windows and macOS builds ship with it compiled in but disabled; you opt in with PYTHON_JIT=1.
  • The Steering Council paused JIT work in June 2026 and asked for a standards-track PEP explaining how the JIT would become a properly supported part of CPython: maintenance, compatibility, tooling, platforms, security.
  • PEP 836 was that proposal. It set a target of at least a 20% geometric-mean speedup (JIT plus free-threaded build versus the free-threaded interpreter) by the first beta of Python 3.17. According to InfoWorld's report, the council found it light on how the JIT would be supported, measured and integrated with debuggers and profilers, and too optimistic about the gains.
  • The authors withdrew it on October 19, citing the high bar and their own limited bandwidth, while saying they still believe in the technical direction.
  • The removal happens on main, which becomes Python 3.16 (first release scheduled for October 2027). Python 3.13, 3.14 and 3.15 keep the JIT for as long as those branches are maintained.

The full text is worth ten minutes of your time, especially the motivation and appendix: PEP 836 on peps.python.org. The PEP itself reports that in Python 3.15 the JIT gives roughly a 4 to 12% geometric-mean improvement across the measured Tier 1 platforms.

So: real gains, small ones, on a benchmark suite, behind an opt-in flag, with a long list of unsolved tooling and maintenance problems. That's the context for my take.

The hot take: for web apps, this is the right call

A JIT speeds up Python bytecode that runs a lot: tight loops, numeric code, long-running CPU-bound work. That's the shape of program it helps most, and InfoWorld's summary says the same thing about this JIT.

A typical Django request is not that shape. Think about where a request to a CRUD endpoint actually spends its time:

  1. Waiting on PostgreSQL (often several round trips).
  2. Waiting on Redis, an external API, or object storage.
  3. Serializing a few hundred objects to JSON.
  4. Template rendering or DRF serializer overhead.
  5. A small slice of your own Python logic.

Only parts of steps 3 to 5 are pure Python execution, and a lot of the time inside them goes to C extensions anyway (the database driver, the JSON encoder you picked, hashlib, and so on). Even a large JIT speedup applied only to the Python slice of a request moves the total much less than the headline number. A single-digit percentage applied to a small slice is noise next to one missing select_related().

Meanwhile, the cost side of an experimental JIT lands on everyone: a build-time LLVM dependency for redistributors, debuggers and profilers that need to understand JIT frames, and more surface for subtle bugs in the runtime you deploy on every server. A runtime that is boring, debuggable and predictable is worth more to me than a few percent I'd never see in a p95 graph.

There's a fair counterargument, and I want to state it properly: if your workload is CPU-bound pure Python (simulation, parsing, some data processing), the JIT was a free speedup on the way and now it's gone from the roadmap. That's a real loss for those teams. But for those workloads the advice was already "push the hot loop into NumPy, a C/Rust extension, or a different process," and that hasn't changed.

What I'd measure instead

If you want your Django app faster, measure where the time goes before thinking about the interpreter. Three cheap checks cover most of it.

1. Count queries per request

A small middleware that logs query count and database time per request finds most real problems:

# myproject/middleware.py import logging import time from django.db import connection log = logging.getLogger("perf") class QueryBudgetMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): queries = [] def timer(execute, sql, params, many, context): start = time.perf_counter() try: return execute(sql, params, many, context) finally: queries.append(time.perf_counter() - start) start = time.perf_counter() with connection.execute_wrapper(timer): response = self.get_response(request) total = time.perf_counter() - start db = sum(queries) log.info( "path=%s status=%s total_ms=%.1f db_ms=%.1f queries=%d python_ms=%.1f", request.path, response.status_code, total * 1000, db * 1000, len(queries), (total - db) * 1000, ) return response 

connection.execute_wrapper() is a documented Django hook, so this works without debug mode. Run it in staging against real-sized data. If an endpoint makes 80 queries, the interpreter is not your problem.

Note that python_ms here still includes time spent waiting on anything that isn't the database (HTTP calls, cache, storage), so treat it as an upper bound on "pure Python" time, not the real number.

2. Profile a real request with a sampling profiler

py-spy attaches to a running process without code changes and shows where wall-clock time goes:

pip install py-spy # Record 30 seconds of a running gunicorn worker while you replay traffic py-spy record --pid <worker-pid> --duration 30 --output profile.svg 

Open the flame graph. In a lot of Django apps the widest bars are in the database driver, serializers and template rendering, not in tight loops of your own code. If you do find a hot pure-Python loop, that's your candidate for an algorithm change, caching, or a compiled extension.

3. Check whether the JIT helps you on 3.15, today

If you're curious, you don't have to guess. On a Python 3.15 build that includes the JIT, run your own benchmark both ways:

# Same build, same machine, same data PYTHON_JIT=0 python manage.py run_my_benchmark PYTHON_JIT=1 python manage.py run_my_benchmark 

Use a management command that exercises your real hot paths (a serializer over a realistic queryset, a report generator), run each several times, and compare medians. If the difference is inside run-to-run noise, the removal costs you nothing. If it's large, you've learned you have a CPU-bound path worth optimizing directly, which is useful regardless of what CPython does.

Where the real speed is for Django apps

None of this is new, which is sort of the point. In rough order of how often it pays off on the projects I see:

  • Fix N+1 queries with select_related() and prefetch_related(), and add the indexes your filters need. Check with EXPLAIN ANALYZE, not intuition.
  • Return less data. .only(), .values(), pagination, and not serializing nested objects the client never reads.
  • Cache what is expensive and rarely changes, with explicit invalidation instead of long blind TTLs.
  • Pool database connections (PgBouncer or Django's built-in pooling for PostgreSQL in 5.1+) so you aren't paying connection setup on every request.
  • Move slow work out of the request into a task queue: emails, PDFs, third-party calls.
  • Size your workers to your CPU and I/O mix, and look at async views only for endpoints that genuinely wait on many external calls.

The core team's own bets also line up better with web workloads than the JIT did: the specializing adaptive interpreter that has been speeding up plain Python since 3.11 (no flag needed), and free-threading, which matters for apps that want real parallelism in one process. Those help you without changing your deployment story.

The short version

  • PEP 836 was withdrawn on October 19. The experimental JIT leaves CPython main (future 3.16). Python 3.13 to 3.15 keep it, still opt-in.
  • For typical Django and DRF apps, request time is dominated by the database, the network and serialization, so a small interpreter speedup was never going to show up much.
  • The cost of carrying an experimental JIT (tooling, debugging, distribution) is real for everyone. Dropping it until someone can meet the bar is a defensible engineering decision.
  • If you want speed this quarter, measure queries per request and profile a real worker. That's where the wins are.

Over to you: have you ever turned on PYTHON_JIT=1 in a real project, and did it make a measurable difference to your app? Or has the interpreter never been the bottleneck in your stack?