Skip to main content

Command Palette

Search for a command to run...

Python 3.15 Is Here: Lazy Imports, a Faster JIT, and the One Change That Might Break Your Code

Updated
•7 min read•View as Markdown
Python 3.15 Is Here: Lazy Imports, a Faster JIT, and the One Change That Might Break Your Code
N
Love to code, gaming. And I use vim btw.

Python 3.15 dropped on October 9th and honestly, this is one of those releases where I had to stop and actually read the changelog instead of skimming it. Lazy imports, a built-in profiler, UTF-8 everywhere, and a JIT that finally benchmarks at double digits — if you're a MERN dev who reaches for Python when you're spinning up an AI pipeline or a data script, this one's worth your morning coffee.

Also: Python 3.10 hit end of life this week. So if your FastAPI microservice or ML script is still running on 3.10, you now have a very good excuse to upgrade.


Lazy Imports Are Now a Language Feature (and They're Exactly What You Think)

The headline feature is PEP 810: a lazy soft keyword that defers a module import until the name is actually used. Until now, getting lazy imports meant reaching for importlib.util.LazyLoader or a third-party library. It worked, but it was ceremonial. Now you just write:

import lazy numpy as np
import lazy pandas as pd
import lazy torch

Nothing loads until you call np.array(...) or pd.DataFrame(...). For a CLI tool or a Lambda function that might not touch its heaviest dependencies on every code path, this matters a lot. Cold starts get shorter. Memory usage on paths that skip certain imports drops to near zero.

A couple of things to know before you start lazy-importing everything:

  • It only works at module scope. You can't use it inside a function body.

  • Errors that would normally happen at import time (a missing module, a broken __init__) now surface at first use instead. Your error logs will look a little different.

  • __lazy_modules__ is the backport-friendly form if you need to support older Pythons alongside 3.15.

For MERN devs who build Express backends that call out to a Python sidecar for ML inference, this is a concrete win: your sidecar's startup time drops, and you can structure your imports around the actual request path rather than loading the entire model pipeline for requests that don't need it.


The UTF-8 Default: The One That'll Actually Bite You

PEP 686 makes UTF-8 the default text encoding for file operations, regardless of system locale. Sounds fine. The problem is that open() without an explicit encoding= argument has been locale-dependent forever, so code that happened to work on English-locale servers might now produce different results on systems where the locale is something else — or, more likely, might now produce different results on Windows developer machines versus Linux CI.

# This now always reads as UTF-8 — everywhere
with open("data.json") as f:
    content = f.read()

# This is the safe form if you need the old behavior
with open("data.json", encoding=locale.getpreferredencoding()) as f:
    content = f.read()

The fix is simple: just pass encoding= explicitly wherever it matters. Run a quick grep across your codebase for bare open( calls. Realistically, most modern Python code is already UTF-8 everywhere, but legacy scripts that write to log files or parse CSVs with unusual characters are the ones to watch.

If you need a temporary escape hatch while you fix things, PYTHONUTF8=0 or -X utf8=0 restores the old locale-based behavior.


The JIT Actually Has Real Numbers Now

Python has had an experimental JIT since 3.13, but "experimental" has been doing a lot of work in that sentence. 3.15 ships with a rewritten tracing frontend, register allocation, and other backend improvements — and for the first time, the benchmarks are worth quoting: 7–8% geometric-mean speedup on x86-64 Linux, and 11–12% on AArch64 macOS.

These are against different baselines (standard interpreter vs. tail-calling interpreter), so they're not directly comparable. But the direction is clear: the JIT is getting faster every cycle and it's now included in the Windows x64 installers by default.

For ML workloads, 8–12% is a real number. If you're running inference on CPU, or doing preprocessing in pure Python before handing off to PyTorch, that compounds across a request-heavy API. It won't replace a proper vectorization pass or GPU offloading, but it's free performance for existing code.


frozendict and sentinel: Two Small Things You'll Use All the Time

Two new builtins that might not make the headlines but will absolutely show up in your daily code:

frozendict is an immutable, hashable mapping. If you've ever tried to use a regular dict as a default argument, a cache key, or a set element and hit a TypeError: unhashable type: 'dict', that's what this is for.

# Before: you had to use tuples or third-party frozendict
DEFAULTS = frozenset([("model", "gpt-4o"), ("temperature", "0.7")])  # ugly

# After: clean and readable
DEFAULTS = frozendict(model="gpt-4o", temperature=0.7)
cache = {}
cache[DEFAULTS] = result  # works

One gotcha: frozendict is not a dict subclass, so isinstance(x, dict) returns False. If you're checking types in your code, update those checks.

sentinel is for those cases where None is a valid value and you need a distinct "no value provided" marker:

_missing = sentinel("missing")

def get_value(key, default=_missing):
    result = store.get(key)
    if result is _missing:
        raise KeyError(key)
    return result

Previously you'd write _missing = object() and hope nobody passed that exact object in from somewhere else. sentinel is proper, readable, and has a helpful repr.


The Tachyon Profiler: Finally a Profiler That Doesn't Require a Restart

PEP 799 ships a new profiling package in the standard library, and the headline piece is Tachyon — a sampling profiler that can attach to a running process. No code changes. No restart. Just:

python -m profiling attach <pid>

It reports wall-clock time, CPU time, or GIL hold time and can output interactive flame graphs. If you've ever had a Python service start behaving slowly in production and wished you could just peer inside without deploying a code change, this is exactly that.

For MERN developers with a Python AI sidecar or a FastAPI endpoint that handles embedding generation, being able to profile the live process is huge. You can now catch the "it's slow in production but not locally" bug class without the gymnastics of injecting cProfile into your deploy.


What This Means for Your Stack

If you're on a MERN backend and Python lives in the same repo as a data processing script, an AI inference layer, or a utility that syncs to a vector store — 3.15 is a low-friction upgrade with real upside.

The three things to do before you upgrade:

  1. Grep for bare open( calls and add explicit encoding="utf-8" where needed. One afternoon of work, zero surprises after.

  2. Try import lazy on your heaviest imports — things like torch, transformers, pandas, or cv2. Measure the cold-start difference.

  3. If you're on Python 3.10, move now. EOL means no security patches. That matters even for internal tooling.

The lazy imports story is the one I'm most excited about. It's one of those features that doesn't feel dramatic until you measure a Lambda cold start before and after, and then you start refactoring your import blocks immediately.


Python has been on a steady improvement arc since 3.10, and 3.15 continues that trajectory with practical wins rather than flashy rewrites. Lazy imports solve a real problem. The UTF-8 default is the right call long-term, even if it causes a short-term grep pass. And the JIT moving from "interesting experiment" to "ships in Windows installers" is the signal that CPython's performance story is no longer a meme.

If you want the full changelog, the official release post is at blog.python.org. Grab 3.15, run your test suite, fix the open() calls, and enjoy the free JIT gains.