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
encodingargument. - The previous encoding default came from your locale, which often meant
cp1252on Windows andutf-8on Linux and macOS. - Passing
encoding="utf-8"keeps your file I/O portable and safe across every Python version and platform. - The
flake8-encodingstool and the-X warn_default_encodingflag help you find code that leans on the implicit default. - Setting
PYTHONUTF8=0or passingencoding="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:
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:
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.
Get Your Code: Click here to download the free sample code you’ll use to find implicit-encoding calls and keep your file I/O portable across every 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 DefaultTest 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.
Note: For a refresher on how text encodings work before you dig in, check out the Unicode & Character Encodings in Python: A Painless Guide tutorial.
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.
Note: The inconsistent text encoding behavior reaches far beyond the built-in open() function. The same encoding rule applies to pathlib methods like Path.read_text() and Path.write_text(), the text modes of gzip.open(), bz2.open(), and lzma.open(), and subprocess calls that pass text=True.
It also affects the text-mode files from tempfile, configparser.ConfigParser.read(), and logging.FileHandler.
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.
A bit of history: This text encoding gap has a long history of tripping up real users. PEP 597 recounts the classic case from the days of setup.py scripts. Package authors often wrote long_description = open("README.md").read() in their setup.py file. This line worked fine on Linux and macOS, but it failed with a UnicodeDecodeError on Windows if the target README.md contained emojis or accented characters.
That specific example is dated now because modern Python projects rely on pyproject.toml instead of setup.py. As a result, this particular bug rarely bites anymore. Still, it captures the root problem perfectly: the same bare open() call decoding differently depending on where it runs. That’s exactly what the new default fixes.
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.