The deployment looked harmless.
We upgraded a Windows export service from Python 3.14 to Python 3.15. The application started correctly, the tests passed, and the scheduled job produced its CSV at the usual time.
Then finance opened the file.
Customer ID,Name,City 1042,Renée,Montréal The records were complete. The columns were in the right order. No exception had been logged.
The code had not changed. Its default encoding had.
What Changed in Python 3.15?
Python 3.15 was released on October 9, 2026. One of its important compatibility changes comes from PEP 686: UTF-8 mode is now enabled by default.
That means text operations without an explicit encoding now use UTF-8, including:
open()- standard input and output
- pipes
- filesystem encoding operations
Before Python 3.15, the default could depend on the operating system and locale. A Windows application might therefore write text using a local encoding such as Windows-1252.
This code contains that dependency:
import csv def export_customers(customers, path): with open(path, "w", newline="") as file: writer = csv.writer(file) writer.writerow(["Customer ID", "Name", "City"]) for customer in customers: writer.writerow([ customer.id, customer.name, customer.city, ]) There is no encoding argument. The export contract is being decided by the Python runtime.
Python 3.15 now makes that decision consistently: UTF-8.
Why Did the Export Succeed?
The failure happened after Python wrote the file.
Consider the name Renée. Its é character is represented differently in UTF-8 and Windows-1252.
Python 3.15 wrote valid UTF-8 bytes. The legacy finance application still assumed Windows-1252, so it interpreted those bytes incorrectly and displayed Renée.
No encoding error occurred because both programs completed their own operations successfully. They simply disagreed about the file format.
This is why encoding bugs can survive unit tests. A test may confirm that the file exists and contains the correct number of rows without checking the bytes another system will receive.
Make Encoding Part of the Export Contract
The correct fix depends on the consumer.
If the receiving system requires Windows-1252, preserve that requirement explicitly:
with open( path, "w", encoding="cp1252", errors="strict", newline="", ) as file: writer = csv.writer(file) Using errors="strict" is intentional. Replacing unsupported characters silently can damage customer names, addresses and product data.
If the consumer accepts UTF-8 but needs a byte-order mark to detect it correctly, use utf-8-sig:
with open( path, "w", encoding="utf-8-sig", newline="", ) as file: writer = csv.writer(file) For ordinary UTF-8 integrations that do not require a byte-order mark, use encoding="utf-8".
The important part is that the decision lives in the code rather than the machine running it.
Test the File, Not Only the Rows
An export test should verify the encoding expected by the receiving application.
import codecs def test_customer_export_uses_utf8_bom(tmp_path): output = tmp_path / "customers.csv" export_customers( customers=[ Customer( id=1042, name="Renée", city="Montréal", ) ], path=output, ) raw = output.read_bytes() assert raw.startswith(codecs.BOM_UTF8) decoded = raw.decode("utf-8-sig") assert "Renée" in decoded assert "Montréal" in decoded This test checks the actual integration boundary. It will fail if someone changes the encoding, removes the required byte-order mark or writes the file using a locale-dependent default.
A fixture should also include characters beyond basic ASCII. Tests containing only names such as John Smith cannot expose this class of problem.
Find Other Hidden Defaults Before Upgrading
Python can warn when code opens text files without specifying an encoding.
Run the test suite with:
python -X warn_default_encoding -m pytest Or enable the same behavior through an environment variable:
PYTHONWARNDEFAULTENCODING=1 python -m pytest Each EncodingWarning deserves review.
Configuration files, templates and application-owned data should usually specify UTF-8. A file that intentionally follows the operating system locale can use:
open(path, encoding="locale") Python 3.15 can temporarily restore the earlier behavior with PYTHONUTF8=0 or -X utf8=0. Those options are useful for comparing behaviors during diagnosis. They should not become the permanent fix for an undefined file contract.
The Upgrade Lesson
Python 3.15 choosing UTF-8 by default is a sensible change. It removes a major source of cross-platform inconsistency.
It also exposes applications that allowed their environment to choose an integration format.
Before upgrading, inspect every place the application exchanges text with another system: CSV exports, fixed-width files, command-line pipes, email attachments, uploaded documents and legacy database imports.
For teams preparing to hire Python developers, encoding migrations are a useful interview topic. Strong candidates should discuss downstream consumers, byte-level tests and compatibility requirements rather than simply adding encoding="utf-8" everywhere.
Python 3.15 did not corrupt the export. It made an undocumented assumption visible.
Which text integration in your application still depends on the machine running it?
