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 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) That's a big deal for CLI tools, where most commands only need a few modules.
Two rules I hit while testing:
-
lazy importis only allowed at module level. Inside a function you getSyntaxError: 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) 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} 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!) 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) 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> 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'tNone -
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.



