Python 3.15 makes UTF-8 the default text encoding. A plain open("notes.txt") call now decodes the same way on every platform, including Windows. Earlier Python versions picked an encoding from your locale, so on Windows the same code that worked on Linux and macOS could produce garbage.

By the end of this tutorial, you’ll understand that:

  • Python 3.15 enables UTF-8 as its default text encoding, so text I/O uses it when you omit the encoding argument.
  • The previous encoding default came from your locale, which often meant cp1252 on Windows and utf-8 on Linux and macOS.
  • Passing encoding="utf-8" keeps your file I/O portable and safe across every Python version and platform.
  • The flake8-encodings tool and the -X warn_default_encoding flag help you find code that leans on the implicit default.
  • Setting PYTHONUTF8=0 or passing encoding="locale" restores the locale-based behavior when you need it.

Here’s what the change looks like. Say that you save the text "Café ☕" to a file and read it back with a bare call to open() on Windows:

Language: Windows PowerShell
PS> py -3.14 -c "print(open('cafe.txt').read())"
Café ☕

In this example, the default decoder—often cp1252—misinterprets the UTF-8 bytes, so the code prints mojibake. Now look at how the same code behaves on Python 3.15:

Language: Windows PowerShell
PS> py -3.15 -c "print(open('cafe.txt').read())"
Café ☕

Same code, no encoding argument to open(), and the mojibake is gone because of the consistent UTF-8 default. In this tutorial, you’ll first see exactly what changes, then try the new default on a Python 3.15 pre-release. After that, you’ll learn how to keep your own code working the same way on every Python version and platform.

Take the Quiz: Test your knowledge with our interactive “Python 3.15 Preview: UTF-8 by Default” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python 3.15 Preview: UTF-8 by Default

Test your understanding of Python 3.15's UTF-8 default, from the origins of the old locale-based encoding to passing explicit encodings in your code.

Meet Python 3.15’s UTF-8 Default

It helps to understand why the default text encoding behavior was a problem in Python versions older than 3.15. Take the built-in open() function as a baseline. Whenever you omitted the encoding argument when opening a text file, Python fell back to whatever locale.getencoding() returned on your operating system.

On Windows, that fallback was the ANSI code page—the legacy character set that Windows selects from your system’s regional settings. Common examples are cp1252 in the Americas and Western Europe, cp1251 in Cyrillic-script regions, and cp932 in Japan.

On Unix, it was the codeset from your LC_CTYPE locale, which was utf-8 on a modern Linux or macOS system but plain ascii under a bare C or POSIX locale. The latter two are the minimal default locales that a system uses when nothing else is configured, which is often the case in containers and CI runners. You end up with the same code but different text encoding or decoding behaviors depending on where the code runs.

This inconsistent behavior can raise UnicodeDecodeError or UnicodeEncodeError when reading or writing text files, respectively. It can also produce mojibake, as you saw earlier, with no error at all.

UTF-8 became the de facto standard everywhere long ago, except in Python’s own default. It’s the standard for source code files, JSON, TOML, and YAML, as well as on the web and in other programming languages like Go, Rust, and Java. On top of that, every mainstream editor, such as Visual Studio Code, uses it out of the box.

Python 3.15 closes that gap through PEP 686. Text input/output (I/O) without an explicit encoding now uses UTF-8 no matter which operating system or locale you’re using.

The table below summarizes the default text encoding that open() picks before and after Python 3.15:

Scenario Default up to 3.14 Default in 3.15+
Windows ANSI code page (cp1252) utf-8
Linux/macOS (UTF-8 locale) utf-8 utf-8
Any OS under a C/POSIX locale ascii utf-8
encoding="<your_preferred_encoding>" as given as given

The rows that change are the ones that used to depend on the operating system or the locale. On a UTF-8 Linux or macOS system, your everyday runs look the same, but the C/POSIX row still applies to you, since containers, cron jobs, and CI runners often start under a bare C locale. Your code also has to decode correctly on other people’s machines. On Windows, this change removes a whole class of bugs.

If you want to go deeper into how character encodings work under the hood, Real Python’s video course on Unicode in Python: Working With Character Encodings covers code points, byte representations, and the built-in functions for converting between them.

Now that you understand the problem, it’s time to try out the new text encoding default on your own.

Try the Default UTF-8 Encoding on a Python 3.15 Pre-Release

To follow along, go ahead and install a Python 3.15 pre-release alongside your existing Python 3.14 or older version. Grab whichever pre-release is current when you read this, since every 3.15 beta and release candidate has UTF-8 mode on by default. If you’ve never set one up before, then the How Can You Install a Pre-Release Version of Python? guide walks through the options.

With both versions available, select your operating system below, then ask each interpreter whether UTF-8 mode is active by reading the sys.flags.utf8_mode flag:

Language: Windows PowerShell
PS> py -3.14 -c "import sys; print(sys.flags.utf8_mode)"
0

PS> py -3.15 -c "import sys; print(sys.flags.utf8_mode)"
1
Language: Shell
$ python3.14-c"import sys; print(sys.flags.utf8_mode)"
0

$ python3.15-c"import sys; print(sys.flags.utf8_mode)"
1

The flag flips from 0 (off) in 3.14 to 1 (on) in 3.15, which confirms that UTF-8 mode is now on by default.

Now create a small text file named cafe.txt containing the non-ASCII text "Café ☕". Rather than relying on your editor’s encoding setting, let Python write the file as UTF-8 for you, then read it back with a bare open() call on each Python version:

Language: Windows PowerShell
PS> py -3.15 -c "open('cafe.txt', 'w', encoding='utf-8').write('Café ☕')"

PS> py -3.14 -c "print(open('cafe.txt').read())"
Café ☕

PS> py -3.15 -c "print(open('cafe.txt').read())"
Café ☕
Language: Shell
$ python3.15-c"open('cafe.txt', 'w', encoding='utf-8').write('Café ☕')"

$ python3.14-c"print(open('cafe.txt').read())"
Café ☕

$ python3.15-c"print(open('cafe.txt').read())"
Café ☕

On Windows, Python 3.14 prints the same mojibake you saw earlier, while Python 3.15 decodes the file as UTF-8 and shows the text you saved. You didn’t change the code, only the interpreter. On Linux and macOS, both versions already print Café ☕.

You don’t have to install both versions on every platform to see the pattern, though. The interactive figure below mirrors these commands. Choose a Python version and platform, then click Run to see what a bare open() prints:

Interactive diagram — enable JavaScript to view.

Whichever combination you try, the same one-line fix makes the behavior predictable, and writing that fix is what you’ll do next.

Make Your Code Encoding-Safe Across Versions

Now that you’ve seen the new default encoding in action, you can make your own code behave safely on every Python version and platform. The durable fix hasn’t changed in years: pass an explicit encoding on every text-mode call.

The workflow has two steps:

  1. Find the spots that rely on the default text encoding.
  2. Pass an explicit encoding at each one.

Finally, if you write Python libraries, one extra practice applies, and you’ll get to it at the end of this section.

Find Code That Relies on the Default Encoding

You have two tools for tracking down implicit-encoding calls, and you should run both. Say that app.py reads a UTF-8 data.json file with a bare open() call as shown below:

Language: Python Filename: app.py
importjson

with open("data.json") as f:
    data = json.load(f)

print(f"Loaded {len(data)} entries")

You can use the flake8-encodings plugin to flag every open(), pathlib.Path, and configparser.ConfigParser call that omits the encoding argument. Install it and run it against your code:

Language: Shell
$ python-mpipinstall"flake8-encodings[classes]"

$ flake8app.py
app.py:3:6: ENC001 no encoding specified for 'open'.

With this output, you know exactly which line to fix. The plugin also checks for open() calls that pass encoding=None, which is equivalent to omitting the argument.

Alternatively, you can rely on a runtime warning to catch the same calls as your code runs. Turn on EncodingWarning, introduced by PEP 597, by passing the -X warn_default_encoding flag to the python command. This way, each implicit-encoding call emits a warning at its own line:

Language: Shell
$ python-Xwarn_default_encodingapp.py
app.py:3: EncodingWarning: 'encoding' argument not specified
  with open("data.json") as f:
Loaded 2 entries

Add -W error::EncodingWarning when you’d rather promote those warnings to hard errors. Your program then stops with a traceback at the first implicit-encoding call instead of running to completion.

If you can’t control how the python command is invoked, then set the PYTHONWARNDEFAULTENCODING environment variable to 1 instead. This setting turns on the same EncodingWarning without any command-line flags.

Between the linter and the runtime warning, you’ll surface every place your code depends on the default, whether or not those lines run during testing.

Pass an Explicit Encoding

Once you’ve found the implicit calls, you can fix each one with a single argument. Pass encoding="utf-8" for text you know is UTF-8. That’s an explicit choice that’s portable and reads clearly on every Python version.

If you want the locale encoding, then pass encoding="locale" instead, which is available on Python 3.10 and later. That value is a sentinel string rather than a codec name. It doubles as a way to silence the warning while documenting that the locale dependence is intentional:

Language: Python
importjson

with open("data.json", encoding="utf-8") as f:  # Portable and unambiguous
    data = json.load(f)

with open("legacy.csv", encoding="locale") as f:  # Intentional locale use
    legacy = f.read()

In this example, the first open() reads data.json as UTF-8, so you get the same result on every platform. The second call opts into the locale encoding, making that older behavior a documented choice rather than an accident.

When several modules read or write text, define the encoding once as a project-wide constant so the choice lives in a single place instead of being scattered across string literals:

Language: Python Filename: constants.py
ENCODING = "utf-8"  # Single source of truth for text I/O

Then import that name wherever you open a file, and switching encodings later becomes a one-line edit:

Language: Python Filename: app.py
importjson

fromconstantsimport ENCODING

with open("data.json", encoding=ENCODING) as f:
    data = json.load(f)

print(f"Loaded {len(data)} entries")

Here, app.py reads the encoding from the shared ENCODING constant instead of hard-coding "utf-8" inline. Every module that imports ENCODING now uses the same value.

When you need the real locale encoding, swap any call to locale.getpreferredencoding(False) for locale.getencoding(). The former now returns "utf-8" under UTF-8 mode and can no longer report the underlying locale:

Language: Python
>>> importlocale

>>> locale.getencoding()  # The real OS encoding, ignores UTF-8 mode
'cp1252'
Language: Python
>>> importlocale

>>> locale.getencoding()  # The real OS encoding, ignores UTF-8 mode
'UTF-8'

The locale.getencoding() function, available on Python 3.11 and later, reports the real locale encoding regardless of UTF-8 mode. In other words, the value you see depends only on your platform and locale, not on whether UTF-8 mode is active.

When you’re not sure which option fits, the table below maps common situations to the right choice:

Situation Encoding Value
Reading or writing a UTF-8 file encoding="utf-8"
Matching the user’s locale encoding encoding="locale"
Reading a known legacy-encoded file encoding="<your_codec>"
Needing the locale encoding outside open() locale.getencoding()

Almost every time, you’ll want encoding="utf-8". You only need the other rows when something specific rules it out.

Forward the Caller’s Encoding in Library Code

If you write libraries, then one more practice matters on top of the two steps above. When some of your functions accept encoding=None and forward it to open(), first wrap the value in a call to io.text_encoding(), which is available on Python 3.10 and later. That call resolves None to the active default encoding:

Language: Shell
$ python3.15-c"import io; print(io.text_encoding(None))"
utf-8

It also makes any EncodingWarning point at your caller rather than a line buried inside your package:

Language: Python Filename: textlib.py
importio


defread_text(path, encoding=None):
    encoding = io.text_encoding(encoding)  # Points at the caller
    with open(path, encoding=encoding) as f:
        return f.read()

To watch that happen, call read_text() from a program that leaves out the encoding argument:

Language: Python Filename: main.py
importtextlib

textlib.read_text("notes.txt")

Run main.py with EncodingWarning enabled, and the warning lands on the call site, not the open() line inside your library:

Language: Shell
$ python-Xwarn_default_encodingmain.py
main.py:3: EncodingWarning: 'encoding' argument not specified
  textlib.read_text("notes.txt")

Comment out the io.text_encoding() line, and that same warning fires inside textlib.py instead, right where the caller can’t act on it. Moving the warning out to the caller is the whole reason to use this technique.

Restore the Previous Behavior When You Need It

Up to this point, you’ve made your code encoding-safe by being explicit. Sometimes, though, you can’t move to UTF-8 at all. Perhaps your project uses a downstream tool or data file that still expects the old locale encoding. Python 3.15 keeps a couple of escape options that can help you manage those scenarios.

The broad one is a switch that turns UTF-8 mode back off. To do this, set the PYTHONUTF8 environment variable to 0 or pass -X utf8=0 when running the python command. This way, the Python interpreter restores the previous locale-based default behavior everywhere:

Language: Windows PowerShell
PS> $env:PYTHONUTF8="0"

PS> py -3.15 -c "import locale; print(locale.getpreferredencoding(False))"
cp1252

PS> py -3.15 -c "print(open('cafe.txt').read())"
Café ☕

Note that this switch revives the original problem. With PYTHONUTF8 set to 0, a bare open("cafe.txt") call on 3.15 behaves exactly as it did on earlier versions, mojibake and all.

The narrow one is the encoding="locale" argument value that you saw earlier, which you can use for a single read or write. It keeps that one call on the locale encoding without touching the rest of your program:

Language: Python
with open("legacy.csv", encoding="locale") as f:
    legacy = f.read()

Use the global switch only while you migrate. You’re better off passing a specific value to encoding because that says what each file needs instead of reconfiguring the whole interpreter. An explicit encoding argument also travels with your code, while an environment variable can go missing on another machine or in another shell.

Conclusion

Python 3.15 makes UTF-8 the default for text I/O through PEP 686. This change ends a long-standing inconsistency where the same code could behave differently on Windows, Linux, or macOS. The new default only affects calls that omit encoding, so the safest move is to be explicit on every text-mode call.

In this tutorial, you’ve learned that:

  • Python 3.15 turns on UTF-8 mode by default, so bare text I/O decodes as UTF-8 on every platform.
  • The change only touches calls that omit the encoding argument, and it leaves explicit encodings alone.
  • Adding encoding="utf-8" keeps your file I/O portable across every Python version, not just 3.15.
  • flake8-encodings and -X warn_default_encoding surface the calls that still rely on the old default.
  • PYTHONUTF8=0 and encoding="locale" bring back the locale-based behavior when you truly need it.

Audit your code with the linter and the warning on the Python you have today, then add explicit encodings. Running the updated code on a 3.15 pre-release confirms the fix and turns the upgrade into a non-event.

For a deeper look at how text and bytes fit together, work through Real Python’s Reading and Writing Files in Python, which moves from file modes to buffering, then revisit Unicode & Character Encodings in Python: A Painless Guide for the encode and decode round trip.

Frequently Asked Questions

Now that you have some experience with Python 3.15’s UTF-8 default, you can use the questions and answers below to check your understanding and recap what you’ve learned.

These FAQs are related to the most important concepts you’ve covered in this tutorial. Click the Show/Hide toggle beside each question to reveal the answer.

Only if it relied on the platform default to decode a non-UTF-8 file, which is common on Windows. Add an explicit encoding, and it behaves the same everywhere.

Set PYTHONUTF8=0 (or -X utf8=0) for the whole process, or pass encoding="locale" on individual calls.

Call locale.getencoding(). It reports the locale encoding regardless of UTF-8 mode, whereas locale.getpreferredencoding(False) now returns "utf-8" under the new default.

Run with -X warn_default_encoding (or PYTHONWARNDEFAULTENCODING=1) to emit an EncodingWarning at each implicit-encoding call. This works on Python 3.10 and later.

When you pass encoding="utf-8", Python always decodes the file as UTF-8, so you get the same result on every machine. In contrast, encoding="locale" hands the decision to the locale encoding, which can differ from one machine to another. Reach for "utf-8" unless you specifically need to match the user’s system setting. The "locale" value is available on Python 3.10 and later.

Take the Quiz: Test your knowledge with our interactive “Python 3.15 Preview: UTF-8 by Default” quiz. You’ll receive a score upon completion to help you track your learning progress:


Interactive Quiz

Python 3.15 Preview: UTF-8 by Default

Test your understanding of Python 3.15's UTF-8 default, from the origins of the old locale-based encoding to passing explicit encodings in your code.