DEV Community

Cover image for Python 3.15 Is Out: 5 New Features You'll Actually Use (Tested)
Muiduzzaman Lipu for Refactorly

Posted on

Python 3.15 Is Out: 5 New Features You'll Actually Use (Tested)

Python 3.15 is out, with a new keyword, a new built-in dictionary type, and a profiler that can look inside a running program. I tested every feature below on Python 3.15.2, and the outputs are real.

Video version (3 minutes):

1. lazy import: load modules only when you use them

lazy import json
import sys

print("json" in sys.modules)   # False: not loaded yet
json.dumps({"ok": True})       # first use → loads now
print("json" in sys.modules)   # True
Enter fullscreen mode Exit fullscreen mode

lazy import json doesn't load the module yet. The moment you actually use json, it loads. You keep normal-looking imports at the top of the file, but you only pay for the ones you use. lazy from decimal import Decimal works too.

Does it matter? I wrote a script that imports 10 standard-library modules (asyncio, http.server, sqlite3, email, …) but only prints hello, and timed 15 runs of each version:

normal imports:  median startup 68.9 ms
lazy imports:    median startup 14.6 ms   (~4.7× faster)
Enter fullscreen mode Exit fullscreen mode

That's a big deal for CLI tools, where most commands only need a few modules.

Two rules I hit while testing:

  • lazy import is only allowed at module level. Inside a function you get SyntaxError: lazy import not allowed inside functions.
  • It's new syntax, so older Pythons fail with SyntaxError: invalid syntax.

2. frozendict: an immutable, hashable dict

config = frozendict(host="localhost", port=8080)

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

cache = {config: "connected"}       # ✅ works as a dict key

config | {"port": 9000}
# frozendict({'host': 'localhost', 'port': 9000})  (a new copy)
Enter fullscreen mode Exit fullscreen mode

Because it can't change, a frozendict is hashable: you can use it as a dict key or put it in a set. A normal dict gives TypeError: cannot use 'dict' as a dict key (unhashable type: 'dict'). Note that frozendict is not a subclass of dict (isinstance(config, dict) is False).

It's a good fit for configuration and constants.

3. * and ** inside comprehensions

rows = [[1, 2], [3], [4, 5, 6]]
[*row for row in rows]
# [1, 2, 3, 4, 5, 6]

dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
{**d for d in dicts}
# {'a': 3, 'b': 2}
Enter fullscreen mode Exit fullscreen mode

Flattening a list of lists no longer needs a double for or itertools.chain. ** merges a list of dicts, and later keys win, just like normal dict merging.

4. sentinel(): a clean "no value given"

Sometimes None is a valid value, so it can't also mean "nothing was passed". People used to write MISSING = object(). Now there's a built-in:

MISSING = sentinel("MISSING")

def get(key, default=MISSING):
    if default is MISSING:
        return "no default given"
    return default

get("x")         # 'no default given'
get("x", None)   # None  (None is a real value here!)
Enter fullscreen mode Exit fullscreen mode

It has a readable name in reprs and error messages (MISSING instead of <object object at 0x…>).

5. Tachyon: a built-in sampling profiler

$ python -m profiling.sampling run slow.py

  sample%  tottime (ms)  function
     96.0       242.000  slow.py:7(slow_search)  🔥
      3.2         8.000  slow.py:3(load_data)
Enter fullscreen mode Exit fullscreen mode

It samples what your program is doing thousands of times per second with very low overhead. On my test script it immediately showed that 96% of the time was in one function. It can also attach to a process that's already running:

python -m profiling.sampling attach <PID>
Enter fullscreen mode Exit fullscreen mode

It can produce flame graphs too (--flamegraph -o out.html).

⚠️ macOS note: it needs elevated permissions there (I got "Unable to access process memory on macOS"), so I ran this example in a Linux container (python:3.15-slim with --cap-add SYS_PTRACE).

Recap

  • lazy import → faster startup (69 ms → 15 ms in my test)
  • frozendict → immutable, hashable dict
  • [*x for x in …] → flatten in one line
  • sentinel() → a "no value" marker that isn't None
  • profiling.sampling → find slow code, even in running apps

There's more in the official release notes, such as UTF-8 as the default encoding. Which feature will you use first?


I post one tested coding tip every day on YouTube at Refactorly. The 30-second version of this one is here.

Top comments (0)