Python 3.15 Makes UTF-8 the Default Encoding: What Breaks and How to Fix It

Python 3.15, released on October 9, 2026, turns on UTF-8 mode by default. Any open() call without an encoding argument now reads and writes UTF-8 instead of whatever the operating system's locale says. On Linux and macOS that is usually what you were getting already. On Windows it isn't, and code that has worked for years can start raising UnicodeDecodeError the day you upgrade. You can test for this today on Python 3.14 without installing anything new.

What PEP 686 changes

The change comes from PEP 686, written by Inada Naoki. Its summary is that Python "consistently uses UTF-8 for default encoding of files, stdio, and pipes." The What's New in Python 3.15 page puts it plainly: I/O operations without an explicit encoding, such as open('flying-circus.txt'), will use UTF-8.

UTF-8 mode itself isn't new. It arrived in Python 3.7 as an opt-in. What changes in 3.15 is the default. According to the os module documentation, when the mode is on:

  • open(), io.open() and codecs.open() default to UTF-8, still with the strict error handler.
  • locale.getpreferredencoding() returns 'utf-8'.
  • sys.stdin, sys.stdout and sys.stderr use UTF-8.
  • Command-line arguments, environment variables and filenames are decoded as UTF-8.

Code that passes encoding= explicitly is unaffected. That one sentence is most of the migration advice.

Why it is mostly a Windows problem

The PEP says it directly: most Unix systems already use a UTF-8 locale, so "this change mostly affects Windows users." With UTF-8 mode off, the Python on Windows documentation explains that Python uses the ANSI code page as the default encoding for text files. That's a legacy Windows code page, not UTF-8. Console I/O and the filesystem encoding on Windows were already UTF-8 even with the mode off, so terminal output and filenames give you no hint that file contents are being written in something else.

The breakage shows up in data files. A CSV, log or config file written by a bare open(path, "w") on Python 3.14 for Windows is stored in the ANSI code page. Read it back with a bare open(path) on 3.15 and Python decodes it as UTF-8. The PEP lists the possible results as "UnicodeError, mojibake, or even silent data corruption."

We reproduced the first one on macOS by writing the file in cp1252, a common Windows code page, explicitly, and reading it back in UTF-8 mode

# What a pre-3.15 Windows install wrote with a bare open(): the ANSI code page.
with open("legacy.csv", "w", encoding="cp1252") as f:
    f.write("Zoë,São Paulo\n")

# The same bare open() call, now running in UTF-8 mode.
with open("legacy.csv") as f:
    print(f.read())

Running it with python3 -X utf8 mixed.py on Python 3.14.7 ends with

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xeb in position 2: invalid continuation byte

cp1252 stores ë as the single byte 0xeb; UTF-8 stores it as 0xc3 0xab. A UTF-8 decoder reads 0xeb as the start of a multi-byte sequence, the next byte doesn't fit, and decoding fails. Bytes that happen to form valid UTF-8 are worse because they decode without any error into the wrong characters.

Preview the 3.15 behaviour on Python 3.14

Because UTF-8 mode has been available since 3.7, -X utf8 (or PYTHONUTF8=1) on your current interpreter gives you the 3.15 default now. This small script shows what each mode reports

import locale, sys

print("utf8_mode:            ", sys.flags.utf8_mode)
print("getpreferredencoding: ", locale.getpreferredencoding(False))
print("getencoding:          ", locale.getencoding())
with open("probe.txt", "w") as f:
    print("open() default:       ", f.encoding)

A macOS shell already uses a UTF-8 locale, so to see a difference we forced the legacy C locale (LC_ALL=C, with PYTHONCOERCECLOCALE=0 so Python doesn't silently switch it to UTF-8). With UTF-8 mode off

utf8_mode:             0
getpreferredencoding:  US-ASCII
getencoding:           US-ASCII
open() default:        US-ASCII

Same locale, with -X utf8

utf8_mode:             1
getpreferredencoding:  utf-8
getencoding:           US-ASCII
open() default:        utf-8

The interesting line is getencoding(). It was added in Python 3.11 and ignores UTF-8 mode, so it always tells you the real locale encoding. Use it when you need to know what the system would have chosen. getpreferredencoding() now just reflects the mode.

Find every implicit encoding before you upgrade

Since Python 3.10, the opt-in EncodingWarning from PEP 597 flags every place that falls back to the default encoding. Turn it on with -X warn_default_encoding or the PYTHONWARNDEFAULTENCODING environment variable. Given this function pair

def save_report(path, rows):
    with open(path, "w") as f:          # no encoding argument
        for name, city in rows:
            f.write(f"{name},{city}\n")

def load_report(path):
    with open(path) as f:               # no encoding argument
        return f.read()

python3 -X warn_default_encoding report.py prints both call sites (paths shortened)

report.py:2: EncodingWarning: 'encoding' argument not specified
  with open(path, "w") as f:          # no encoding argument
report.py:7: EncodingWarning: 'encoding' argument not specified
  with open(path) as f:               # no encoding argument

In a test suite, add -W error::EncodingWarning so the warning becomes an exception and CI fails on any call you missed. For a static check, Ruff has an unspecified-encoding rule (PLW1514). It's still a preview rule, and its autofix is marked unsafe because adding encoding="utf-8" can change behaviour on machines whose default isn't UTF-8. That warning is the whole point of this upgrade, so review each fix rather than applying them in bulk.

How to fix each call

For each flagged call, decide what encoding the data actually uses:

  • Files your program creates and reads itself, plus anything exchanged with other systems: use encoding="utf-8". It behaves the same on 3.14 and 3.15 and on every OS.
  • Existing files written by older code on Windows: open them with the encoding they were written in, such as encoding="cp1252", and consider converting them once.
  • Files that really should follow the user's system settings, like output for a legacy Windows tool: use encoding="locale", supported since 3.10. On Windows the docs also note the "mbcs" codec still reaches the ANSI code page while UTF-8 mode is on.
from pathlib import Path

with open("legacy.csv", "w", encoding="cp1252") as f:
    f.write("Zoë,São Paulo\n")

# Old data: say what it actually is.
with open("legacy.csv", encoding="cp1252") as f:
    print(f.read(), end="")

# New data: pin UTF-8 so 3.14 and 3.15 behave the same.
Path("report.csv").write_text("Zoë,São Paulo\n", encoding="utf-8")
print(Path("report.csv").read_text(encoding="utf-8"), end="")
$ python3 -X utf8 fixed.py
Zoë,São Paulo
Zoë,São Paulo

Don't forget subprocess. With text=True and no encoding, the subprocess documentation says pipes use the io.TextIOWrapper defaults, and PEP 686 moves those to UTF-8 too. If you capture output from a tool that doesn't write UTF-8, pass its encoding explicitly.

The escape hatch, and why not to rely on it

If you have to ship 3.15 before the code is fixed, PYTHONUTF8=0 or -X utf8=0 restores the old locale-based behaviour. The os documentation notes it can only be set at startup, and sys.flags.utf8_mode shows the current value. That's fine as a stopgap for a service you control. It's a poor long-term answer because the next person to run the script won't know about the variable. Explicit encoding= arguments make the code behave the same on every version and platform, and they're the version of this fix that won't come back.

WINDOWS PYTHON 3.15 UTF-8 MODE PEP 686 ENCODINGWARNING UNICODEDECODEERROR

  RELATED

  COMMENTS

0

No comment for this article.