What Is New in Python 3.15
Python 3.15 makes UTF-8 the default I/O encoding on all platforms (ending the long-standing platform-dependent behavior), ships a built-in high-frequency sampling profiler, introduces the PyBytesWriter C API for efficient buffer building, and improves AttributeError suggestions across nested members. Several long-deprecated items are removed.
| Category | Change | PEP / Reference |
|---|---|---|
| Encoding | UTF-8 is now the default encoding for all I/O when no encoding is specified | PEP 686 |
| New Modules | profiling.sampling -- high-frequency sampling profiler, zero source changes |
PEP 799 |
| C API | PyBytesWriter API for building bytes objects without reallocations |
PEP 782 |
| Error Messages | AttributeError suggests attributes of nested members when a name is not found at top level |
-- |
| Standard Library | Improvements to calendar, collections, hashlib, sqlite3 |
-- |
| Removed | Old NamedTuple keyword-argument syntax; zipimport legacy methods; collections.abc.ByteString from __all__ |
-- |
| C API | Cleanup: newer functions replace older PyUnicode and PyImport APIs |
-- |
What Does Defaulting to UTF-8 Mean in Practice?
Before Python 3.15, open("file.txt") used the platform's locale encoding -- UTF-8 on most Linux systems, but often Latin-1, CP1252, or another encoding on Windows or older macOS. This caused files written on one system to be read incorrectly on another depending purely on locale settings. PEP 686 makes UTF-8 the default everywhere when no encoding= argument is given.
# Before 3.15: encoding depends on platform locale
with open("data.txt") as f: # could be UTF-8, Latin-1, CP1252...
content = f.read()
# Python 3.15+: always UTF-8
with open("data.txt") as f: # guaranteed UTF-8
content = f.read()
# If you truly need the platform encoding:
import locale
with open("data.txt", encoding=locale.getpreferredencoding(False)) as f:
content = f.read()
This breaks code that wrote Latin-1 or CP1252 encoded files and reads them back without specifying encoding=. The fix is explicit: always pass encoding="latin-1" (or whatever the file actually is). Code that already specifies encoding= is unaffected.
How Does the Sampling Profiler Work?
The new profiling.sampling module is a statistical profiler that interrupts execution at a configurable frequency (up to ~1,000,000 Hz) to record the current call stack. Unlike cProfile (which instruments every function call), sampling adds minimal overhead -- typically under 1% -- and can attach to a running process without restarting it.
import profiling.sampling as sp
profiler = sp.Sampler(interval=0.001) # 1ms = 1000 Hz
profiler.start()
run_workload()
profiler.stop()
stats = profiler.get_stats()
stats.print_top(20) # top 20 hotspots by sample count
Sampling profilers are the right tool for production profiling where cProfile's call-count instrumentation overhead is unacceptable. The profiler works with both GIL and free-threaded builds, and with the JIT enabled.
What Is the PyBytesWriter C API?
The PyBytesWriter API (PEP 782) provides C extensions with a way to incrementally build a bytes object without multiple allocations and copies. The old pattern was to allocate a large buffer, fill it, then resize -- or use PyBytes_FromStringAndSize on a complete buffer. PyBytesWriter manages a growable buffer internally and produces the final bytes object in one step.
// C extension code (conceptual)
PyBytesWriter *writer = PyBytesWriter_Create(1024);
PyBytesWriter_WriteBytes(writer, header, header_len);
PyBytesWriter_WriteBytes(writer, payload, payload_len);
PyObject *result = PyBytesWriter_Finish(writer); // single allocation
This is relevant for C extension authors writing serializers, encoders, or protocol packers -- not for Python-level code, where bytearray and bytes.join() serve the same purpose.
What Was Removed or Deprecated in Python 3.15?
- Old
NamedTuplekeyword-argument syntax (usingNamedTuple("Name", field=type)instead of class form) is removed -- use the class-based form:class Point(NamedTuple): x: int; y: int. - Legacy
zipimportmethods that were already deprecated are removed. collections.abc.ByteStringis removed fromcollections.abc.__all__(was deprecated since 3.12) -- useUnion[bytes, bytearray, memoryview]explicitly.- Various undocumented C API functions replaced by the newer stable ABI equivalents.
RLock()will silently ignore any positional/keyword arguments -- warnings issued in 3.14 become enforced in 3.15+.
FAQ
Will the UTF-8 default change break my code that reads files without specifying encoding?
Only if those files contain non-UTF-8 characters (Latin-1, Windows CP1252, etc.). If all your files are already UTF-8 (typical for any code written or updated in the last decade), nothing changes. The safest migration: grep your codebase for bare open( calls without encoding= and add explicit encoding arguments where needed.
Is profiling.sampling better than cProfile for all use cases?
Different tools for different jobs. cProfile gives exact call counts and cumulative times -- essential when you need to know exactly how many times a function was called. Sampling gives low-overhead statistical hot-spot detection -- essential in production or when the overhead of instrumentation would change the timing being measured. Use both: sampling to find where time goes, then cProfile to confirm call counts in the hot path.
Does the UTF-8 default apply to sys.stdin and sys.stdout as well?
UTF-8 mode for sys.stdin/stdout/stderr was already available via PYTHONUTF8=1 or python -X utf8 (PEP 540, Python 3.7). PEP 686 extends this to file open() calls. In Python 3.15, both are UTF-8 by default. If you need binary-mode stdin, use sys.stdin.buffer.
Can I use the sampling profiler on production systems without restarting the process?
Yes -- attaching to a running process is one of the stated design goals. The profiler can be triggered via a signal or an out-of-band channel. Sampling at 1 kHz adds under 1% overhead on typical workloads, making it safe for production profiling sessions.
What is the class-based NamedTuple syntax that replaces the deprecated form?
Define it as a class inheriting from NamedTuple with annotated fields: class Point(NamedTuple): x: float; y: float; label: str = "origin". This has been supported since Python 3.6 and is strictly cleaner -- it supports default values, docstrings, and method definitions. The functional form Point = NamedTuple("Point", [("x", float), ("y", float)]) still works and is not deprecated.