Python 3.15 is not officially released yet.

As of 18 May 2026, it has reached beta 1. According to the official release schedule, no new features will be added after this point, and the final release is expected on 1 October 2026.

In other words, now is a good time to look at what Python 3.15 is bringing to us.

Compared with Python 3.14, this version may not look as flashy at first glance. But after reading through the official documentation, I found many updates that can genuinely improve our daily programming experience.

Some of them make Python code start faster. Some make old idioms cleaner. Some help us debug production performance problems. And some continue Python's long journey toward better typing and better runtime performance.

As an old Python developer, I handpicked 9 updates that I think are worth your attention.

Now, let's taste them together in this article.

1. Lazy Imports: Make Large Python Apps Start Faster

Import time is a silent performance killer.

For a small script, importing a few modules is not a problem. But for a large application with a deep dependency tree, importing everything at startup can easily waste seconds, even if many of those modules are never used in that run.

Before Python 3.15, we usually solved this problem by moving imports into functions:

def export_report():
    import pandas as pd
    ...

This works, but it also makes the code less clean. Imports are scattered everywhere, and after a while, nobody knows whether an import is local for performance reasons or just written there casually.

This is why Python 3.15 introduces explicit lazy imports:

lazy import json
lazy from pathlib import Path

print("Starting up...")

data = json.loads('{"name": "Yang"}')  # json is loaded here
path = Path(".")                       # pathlib is loaded here

As the above code shows, the new lazy soft keyword tells Python not to load the module immediately.

Instead, Python creates a lightweight proxy. The real import happens only when the imported name is first used.

This is especially useful for command-line tools, web apps, and data-processing applications where not every feature is used every time.

For example:

lazy import pandas as pd
lazy from myapp.reports import build_pdf

def main(command: str):
    if command == "serve":
        start_server()
    elif command == "export":
        df = pd.DataFrame(load_data())
        build_pdf(df)

If the user only runs the server, pandas and the PDF module do not need to slow down the startup path.

Python 3.15 also provides global switches such as -X lazy_imports, the PYTHON_LAZY_IMPORTS environment variable, and sys.set_lazy_imports() for more advanced control.

But my personal suggestion is: use explicit lazy imports first since Python 3.15.

They are clear. They are local. And they tell the next developer exactly what you are optimizing.

2. frozendict: A New Immutable Dictionary in Builtins

Python has immutable tuples, immutable strings, immutable bytes, and immutable frozensets.

But for a long time, it did not have an immutable dictionary as a built-in type.

Python 3.15 fixes this by adding frozendict.

config = frozendict(
    host="localhost",
    port=2077,
    debug=False,
)

print(config["host"])
# localhost

config["debug"] = True
# TypeError: 'frozendict' object does not support item assignment

3. sentinel: A Cleaner Way To Represent Missing Values

Unique placeholder values, often called "sentinel values", are commonly used in programming to represent missing values or indicate that an argument was not provided.

As to Python, sometimes we may use None as a sentinel value, but eventually None is a valid value. It's not easy to see the default value is None itself or completely "N/A" ("not available").

A classic trick is like this:

MISSING = object()

def get_value(key, default=MISSING):
    ...

The expression MISSING = object() creates a unique sentinel object. Because every call to object() creates a new, distinct memory address, it guarantees that MISSING is absolutely unique and will never equal any other value, even if a variable or default function argument evaluates to None, {}, or [].

It seems practical to distinguish between "a value was explicitly set to None" and "no value was provided at all".

However, the problem is that this object does not have a friendly representation. It is also not ideal for typing, copying, pickling, and documentation.

Fortunately, Python 3.15 adds a built-in sentinel type which makes everything more elegant:

MISSING = sentinel("MISSING")

def get_setting(name: str, default=MISSING):
    value = read_from_env(name)

    if value is MISSING:
        return default

    return value

The representation is much better:

>>> MISSING
MISSING

This may look like a tiny feature, but it could remove lots of small awkwardness in APIs, configuration systems, serializers, database wrappers, and so on.

4. Unpacking in Comprehensions: Flatten Data More Elegantly

Python comprehensions are beautiful, but flattening nested data has always been a little awkward.

For example, given a list of lists:

groups = [
    ["Yang", "Bob"],
    ["Cindy"],
    ["David", "Eve"],
]

Before Python 3.15, we usually wrote:

names = [name for group in groups for name in group]

It works, but the order of the two for clauses is not very intuitive, especially for beginners.

Can Python do it better?

Yes, of course. Python 3.15 supports unpacking directly in comprehensions:

names = [*group for group in groups]

print(names)
# ['Yang', 'Bob', 'Cindy', 'David', 'Eve']

It also works for sets:

permissions = [
    {"read", "write"},
    {"read", "delete"},
]

all_permissions = {*p for p in permissions}
# {'read', 'write', 'delete'}

And for dictionaries:

configs = [
    {"host": "localhost"},
    {"port": 2077},
    {"debug": False},
]

config = {**c for c in configs}
# {'host': 'localhost', 'port': 2077, 'debug': False}

This feature is a natural extension of the asterisks unpacking technique we already use:

[*a, *b, *c]
{**a, **b, **c}

Now, bringing it into comprehensions makes Python 3.15 even more elegant than its predecessors.

5. UTF-8 Becomes the Default Encoding

This one is easy to underestimate.

Starting from Python 3.15, UTF-8 is the default encoding, independent of the system environment.

That means, for instance, the following code will use UTF-8 by default:

with open("users.txt") as f:
    data = f.read()

This is a welcome change. UTF-8 has already become the standard encoding for the web, source code, JSON, Markdown, configuration files, and most modern data formats.

However, as far as I am concerned, it does not mean we should stop writing explicit encodings.

For production code, I still prefer this:

with open("users.txt", encoding="utf-8") as f:
    data = f.read()

Why?

Explicit is better than implicit.

— The Zen of Python

Also, if you are maintaining a project that still depends on the system locale encoding, Python 3.15 provides a compatibility path:

with open("legacy.txt", encoding="locale") as f:
    data = f.read()

Or you can disable UTF-8 mode through PYTHONUTF8=0 or -X utf8=0.

In short, this update makes the default behavior more modern, but explicit encoding is still the best habit.

6. A New profiling Package and the Tachyon Sampling Profiler

Python 3.15 adds a new profiling package.

It organizes Python's profiling tools under a clearer namespace:

import profiling.tracing

The old cProfile module still works as a backward-compatible alias, so existing code does not need to panic.

But the more exciting part is profiling.sampling, also known as Tachyon.

Unlike deterministic profilers that trace every function call, Tachyon samples stack traces periodically. This means it can profile running Python processes with very low overhead. No code modification or process restart is required.

For example, we can profile a script directly:

python -m profiling.sampling run app.py

Generate a flame graph:

python -m profiling.sampling run --flamegraph -o profile.html app.py

Or attach to a running process:

python -m profiling.sampling attach 12345

This is very practical.

In many real systems, the slowest problem does not happen when we are ready with our profiler. It happens in a process that is already running, serving real traffic, and behaving strangely.

Restarting it with special profiling code is often not acceptable.

Tachyon gives us a standard-library way to ask:

"What is this Python process doing now?"

It also supports useful modes such as wall-clock time, CPU time, GIL-holding time, exception time, async-aware profiling, and opcode-level profiling.

This is not a small feature. It can significantly improve how Python developers investigate production performance problems.

7. Better Error Messages for Common Mistakes

Python's error messages have improved a lot in recent versions, and Python 3.15 continues this direction.

For example, if a developer coming from JavaScript writes:

names = ["Charlie Munger", "Warrent Buffet"]
names.push("Yang Zhou")

Python can suggest the correct method:

AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?

Another common mistake:

"Yang".toUpperCase()

Python can suggest:

AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean '.upper'?

This is friendly for beginners, but not only for beginners.

Experienced developers also switch languages. We write JavaScript today, Python tomorrow, SQL the day after tomorrow. Our fingers remember APIs from different ecosystems.

Good error messages save us time.

8. The JIT Compiler Gets More Serious

Python 3.14 introduced an experimental JIT compiler.

Python 3.15 makes it much stronger.

According to the official documentation, the Python 3.15 JIT has a new tracing frontend, basic register allocation, more optimizations, better machine code generation, and support for unwinding through JIT frames in tools such as GDB on supported platforms.

The benchmark numbers are not final, but the official docs report an 8–9% geometric mean performance improvement for JIT builds over the standard CPython interpreter on x86–64 Linux, and 12–13% on AArch64 macOS compared with the tail-calling interpreter.

This does not mean all Python programs will magically become 10% faster.

Performance depends on the workload. Some code can benefit a lot, while some code may not benefit or may even slow down.

But the direction is significant, and we as developers should be aware of it.

Python is gradually gaining more serious runtime optimization capabilities without changing the language we write every day.

For most developers, the practical takeaway for now is simple:

Do not rewrite your code just because the JIT exists.

But keep watching it.

If your workload is CPU-heavy and mostly Python code, Python 3.15's JIT improvements may become interesting for benchmarking.

9. Better Typing: TypeForm, Closed TypedDict, and More

Python's type system keeps evolving.

Python 3.15 brings several typing updates, and two of them are especially worth noticing.

First, TypeForm helps annotate values that are themselves type expressions. As mentioned on PEP 747, TypeForm[T] means "a type form object describing T (or a type assignable to T)". At runtime, TypeForm(x) simply returns x, which allows explicit annotation of type-form values without changing behavior.

A quick example:

from typing import Any, TypeForm

def cast[T](typ: TypeForm[T], value: Any) -> T: ...

This is useful for libraries that work with runtime type expressions while still wanting precise static types.

Second, TypedDict now supports closed and extra_items.

A closed TypedDict does not allow extra keys beyond the keys declared in the class body.

This helps model strict JSON-like objects:

from typing import TypedDict

class User(TypedDict, closed=True):
    id: int
    name: str

On the other hand, extra_items allows additional keys, but constrains their value type:

class Metrics(TypedDict, extra_items=float):
    total: float

That means total is required by the class definition, while other keys can exist as long as their values are floats.

For everyday scripts, this may not matter much.

But for large codebases, API clients, validation libraries, and frameworks, these features make Python's static typing more expressive and more accurate.

It is another step toward a more practical type system, not a noisier one.

Conclusion

Python 3.15 is not just a collection of small syntax changes.

It improves Python in several important directions:

  • Faster startup through lazy imports.
  • Safer immutable data with frozendict.
  • Cleaner missing-value APIs with sentinel.
  • More elegant comprehensions.
  • A modern UTF-8 default.
  • Much better profiling tools.
  • Friendlier error messages.
  • A more capable JIT compiler.
  • A more expressive type system.

If I had to summarize Python 3.15 in one sentence, I would say:

Python 3.15 is faster and better, and more elegance.

— Yang Zhou

That is exactly why many of us love it.

The final release is still scheduled for 1 October 2026, so some details may still be polished before then. But the feature set is already stable after beta 1, and it is worth testing your libraries and applications early.

As always, the best way to learn a new Python version is not merely reading the release notes.

Install it. Run your tests. Try the new syntax. Benchmark your own code.

Then you will know which updates truly matter to you.

Thanks for reading. ❤️

I'm Yang Zhou, the founder of Modern Python.

Every Friday, Modern Python delivers a carefully curated briefing covering the most important Python releases, AI developments, tools, and tutorials, so you can stay ahead without spending hours scrolling through news, social media, and documentation.

Subscribe here for free: