What the changed default does and does not convert

Python 3.15 was released on 9 October 2026, and default UTF-8 is an important migration topic for this branch. Teams exchanging Persian files between Windows, Linux and containers should review their text boundaries. An interpreter upgrade does not convert the contents of an existing legacy file. Identify who writes and reads each file and which encoding that interface expects before treating the new default as a complete migration strategy.

encoding_probe reports the Python version, UTF-8 mode, locale encoding and filesystem encoding. Environment configuration can affect the actual behavior, so a version number alone does not establish every default. Run the probe inside the service environment as well. A development terminal does not establish how a containerized process or Windows service is configured. Record the observation alongside the deployment evidence rather than relying on the workstation's locale.

Explicit contracts for new and legacy files

For new files, specify encoding at the read and write boundary. The JSON report example uses UTF-8 and exclusive creation so an existing file is not accidentally replaced. ensure_ascii=False retains readable Persian characters in the file. The test reads that same file back and compares the original structure. A visually plausible display in one editor is not sufficient evidence that the file contract works across processes and operating systems.

Read a legacy file using a known source encoding instead of automatic guesses or silent character replacement. convert_legacy applies a size limit, refuses identical source and destination paths and uses strict decoding so failure remains visible. It creates a separate UTF-8 destination. This implementation targets bounded small inputs. Larger transfers need a streaming converter that retains the same explicit encoding, failure and output-publication rules rather than removing the size guard.

Tests round-trip Persian text, convert a known CP1256 fixture and verify that a decode error does not create output. They also exercise the input-size limit and preservation of an existing report. The example deliberately avoids errors='replace': a superficially successful conversion can otherwise change data silently. If real inputs contain several encodings, distinguish them in the input contract and quarantine ambiguous files for investigation instead of treating every byte sequence as interchangeable text.

Complete files, execution and tests

File-handling logic: Python 3.11+; observing the changed default: Python 3.15 with recorded environment settings. Save the four complete files below together using the stated filenames; no code is replaced by ellipses.

1. Observe the actual runtime and encoding mode

File: encoding_probe.py. Save this complete file without removing lines.

import json
import locale
import sys


report = {
    'python': sys.version.split()[0],
    'utf8_mode': bool(sys.flags.utf8_mode),
    'locale_encoding': locale.getencoding(),
    'filesystem_encoding': sys.getfilesystemencoding(),
    'target_default_changed_in': '3.15',
}
print(json.dumps(report, ensure_ascii=True))

python encoding_probe.py prints the current environment as JSON. To observe the 3.15 default, run it using that interpreter and the service configuration.

2. Explicit reads, reports and file conversion

File: text_files.py. Save this complete file without removing lines.

import json
from pathlib import Path


def read_utf8(path):
    return Path(path).read_text(encoding='utf-8', errors='strict')


def write_report(path, value):
    with Path(path).open('x', encoding='utf-8', newline='\n') as output:
        json.dump(value, output, ensure_ascii=False, indent=2)
        output.write('\n')


def convert_legacy(source, destination, *, source_encoding, max_bytes=4 * 1024 * 1024):
    if max_bytes < 1:
        raise ValueError('max_bytes must be positive')
    source, destination = Path(source).resolve(), Path(destination).resolve()
    if source == destination:
        raise ValueError('Preserve the original source file')
    with source.open('rb') as original:
        raw = original.read(max_bytes + 1)
    if len(raw) > max_bytes:
        raise ValueError('Input exceeds the configured size limit')
    text = raw.decode(source_encoding, errors='strict')
    with destination.open('x', encoding='utf-8', newline='') as output:
        output.write(text)
    return destination

This example caps input at four megabytes. It preserves the source, requires a declared source encoding and creates destinations with exclusive mode.

3. Persian-text and failure-path tests

File: test_text_files.py. Save this complete file without removing lines.

from pathlib import Path
import json
import tempfile
import unittest
from text_files import convert_legacy, read_utf8, write_report


class EncodingTests(unittest.TestCase):
    def test_persian_json_roundtrip(self):
        with tempfile.TemporaryDirectory(prefix='liyan-utf8-json-') as folder:
            path = Path(folder) / 'report.json'
            expected = {'name': 'لیان', 'message': 'سلام دنیا'}
            write_report(path, expected)
            self.assertEqual(json.loads(read_utf8(path)), expected)
            with self.assertRaises(FileExistsError):
                write_report(path, {'name': 'overwrite'})
            self.assertEqual(json.loads(read_utf8(path)), expected)

    def test_explicit_legacy_conversion(self):
        with tempfile.TemporaryDirectory(prefix='liyan-utf8-legacy-') as folder:
            old, new = Path(folder) / 'legacy.txt', Path(folder) / 'utf8.txt'
            old.write_bytes('سلام'.encode('cp1256'))
            convert_legacy(old, new, source_encoding='cp1256')
            self.assertEqual(read_utf8(new), 'سلام')
            self.assertNotEqual(old.read_bytes(), new.read_bytes())

    def test_decode_error_and_size_limit_do_not_create_output(self):
        with tempfile.TemporaryDirectory(prefix='liyan-utf8-failure-') as folder:
            old, new = Path(folder) / 'bad.txt', Path(folder) / 'new.txt'
            old.write_bytes(bytes([255]))
            with self.assertRaises(UnicodeDecodeError):
                convert_legacy(old, new, source_encoding='utf-8')
            self.assertFalse(new.exists())
            old.write_bytes(b'12345')
            with self.assertRaises(ValueError):
                convert_legacy(old, new, source_encoding='ascii', max_bytes=4)
            self.assertFalse(new.exists())


if __name__ == '__main__':
    unittest.main()

Run python -m unittest test_text_files.py. A decode error or oversized input must not create a destination, and an existing report must survive.

4. A complete sample migration

File: migrate_demo.py. Save this complete file without removing lines.

import json
from pathlib import Path
import tempfile
from text_files import convert_legacy, read_utf8, write_report


with tempfile.TemporaryDirectory(prefix='liyan-encoding-demo-') as folder:
    source = Path(folder) / 'legacy.txt'
    destination = Path(folder) / 'utf8.txt'
    source.write_bytes('سلام'.encode('cp1256'))
    convert_legacy(source, destination, source_encoding='cp1256')
    report = {'converted_text': read_utf8(destination), 'original_preserved': source.exists()}
    report_path = Path(folder) / 'report.json'
    write_report(report_path, report)
    assert json.loads(read_utf8(report_path)) == report
    print(json.dumps(report, ensure_ascii=True))

python migrate_demo.py converts a controlled CP1256 fixture to UTF-8 and reads its report back. Console JSON uses ASCII escapes so terminal rendering is not confused with file correctness.

Test Persian data before upgrading a runtime

Save the four files together and run python -m unittest test_text_files.py. The explicit application logic also works on Python 3.11+, while the changed default must be observed separately with a 3.15 interpreter. migrate_demo.py checks a conversion and a JSON report. Before upgrading a service, retain representative legacy data, Persian filenames, CSV exports and inter-process text exchanges in the acceptance suite so future changes cannot erase the migration guarantees.

Implementation checklist

  • Specify encoding at file boundaries.
  • Run the probe in the actual service runtime.
  • Preserve the source and an existing destination.
  • Test strict decoding, size limits and Persian round trips.

References

Editorial guidance: Liyan Knowledge.