This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new aeadc0692191 [SPARK-57978][PYTHON] Use explicit UTF-8 encoding in 
text-mode I/O to prepare for PEP 686
aeadc0692191 is described below

commit aeadc0692191a07add6a499a2360c531a6b0e38e
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Mon Jul 6 21:43:55 2026 -0700

    [SPARK-57978][PYTHON] Use explicit UTF-8 encoding in text-mode I/O to 
prepare for PEP 686
    
    ### What changes were proposed in this pull request?
    
    This PR adds an explicit `encoding="utf-8"` to all text-mode I/O in the 
PySpark runtime (non-test) code.
    
    Note that
    - The JVM <-> Python boundary is also unchanged: it already uses explicit 
UTF-8 (`StandardCharsets.UTF_8` on the Scala side, binary sockets with 
`UTF8Deserializer` on the Python side).
    - the test code and doctests are unchanged (they read and write with the 
same default, so they are self-consistent).
    
    ### Why are the changes needed?
    
    [PEP 686](https://peps.python.org/pep-0686/) will make UTF-8 mode the 
default in Python 3.15, changing the default encoding of text-mode I/O from the 
locale encoding to UTF-8. On non-UTF-8 locales (Windows cp1252/cp949, POSIX 
`C`), the same code would behave differently on Python <= 3.14 and 3.15. 
Explicit encoding pins the behavior across all versions and locales.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. On UTF-8 locales the behavior is identical; on non-UTF-8 locales, files 
written by PySpark are now consistently UTF-8, matching the Python 3.15 default.
    
    ### How was this patch tested?
    
    Pass the CIs.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Fable 5
    
    Closes #57056 from dongjoon-hyun/SPARK-57978.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 python/packaging/classic/setup.py                    | 4 ++--
 python/packaging/client/setup.py                     | 2 +-
 python/packaging/connect/setup.py                    | 2 +-
 python/pyspark/daemon.py                             | 6 ++++--
 python/pyspark/errors/error_classes.py               | 4 +++-
 python/pyspark/errors/exceptions/__init__.py         | 2 +-
 python/pyspark/errors_doc_gen.py                     | 2 +-
 python/pyspark/ml/connect/io_utils.py                | 4 ++--
 python/pyspark/ml/deepspeed/deepspeed_distributor.py | 4 +++-
 python/pyspark/ml/dl_util.py                         | 2 +-
 python/pyspark/ml/torch/distributor.py               | 6 +++---
 python/pyspark/ml/torch/torch_run_process_wrapper.py | 2 +-
 python/pyspark/pandas/supported_api_gen.py           | 2 +-
 python/pyspark/pipelines/cli.py                      | 4 ++--
 python/pyspark/pipelines/init_cli.py                 | 6 +++---
 python/pyspark/profiler.py                           | 2 +-
 python/pyspark/shell.py                              | 2 +-
 python/pyspark/shuffle.py                            | 2 +-
 python/pyspark/sql/profiler.py                       | 2 +-
 python/pyspark/testing/sqlutils.py                   | 3 ++-
 python/pyspark/util.py                               | 2 +-
 21 files changed, 36 insertions(+), 29 deletions(-)

diff --git a/python/packaging/classic/setup.py 
b/python/packaging/classic/setup.py
index 53d11a917f55..97ec9dd90ddb 100755
--- a/python/packaging/classic/setup.py
+++ b/python/packaging/classic/setup.py
@@ -45,7 +45,7 @@ if (
     os.chdir(Path(__file__).parent.parent.parent.absolute())
 
 try:
-    exec(open("pyspark/version.py").read())
+    exec(open("pyspark/version.py", encoding="utf-8").read())
 except IOError:
     print(
         "Failed to load PySpark version file for packaging. You must be in 
Spark's python dir.",
@@ -258,7 +258,7 @@ try:
     # will search for SPARK_HOME with Python.
     scripts.append("pyspark/find_spark_home.py")
 
-    with open("README.md") as f:
+    with open("README.md", encoding="utf-8") as f:
         long_description = f.read()
 
     setup(
diff --git a/python/packaging/client/setup.py b/python/packaging/client/setup.py
index 9c7df16d9537..6c97164937ee 100755
--- a/python/packaging/client/setup.py
+++ b/python/packaging/client/setup.py
@@ -142,7 +142,7 @@ try:
     _minimum_pyyaml_version = "3.11"
     _minimum_zstandard_version = "0.25.0"
 
-    with open("README.md") as f:
+    with open("README.md", encoding="utf-8") as f:
         long_description = f.read()
 
     connect_packages = [
diff --git a/python/packaging/connect/setup.py 
b/python/packaging/connect/setup.py
index 3df383f23a04..a4e83e41d151 100755
--- a/python/packaging/connect/setup.py
+++ b/python/packaging/connect/setup.py
@@ -95,7 +95,7 @@ try:
     _minimum_pyyaml_version = "3.11"
     _minimum_zstandard_version = "0.25.0"
 
-    with open("README.md") as f:
+    with open("README.md", encoding="utf-8") as f:
         long_description = f.read()
 
     connect_packages = [
diff --git a/python/pyspark/daemon.py b/python/pyspark/daemon.py
index ef32bf760f47..0e321d442e81 100644
--- a/python/pyspark/daemon.py
+++ b/python/pyspark/daemon.py
@@ -98,7 +98,9 @@ def worker(sock: socket.socket, authenticated: bool) -> int:
                 faulthandler_log_path = 
os.environ.get("PYTHON_FAULTHANDLER_DIR", None)
                 if faulthandler_log_path:
                     faulthandler_log_path = 
os.path.join(faulthandler_log_path, str(os.getpid()))
-                    with open(faulthandler_log_path, "w") as 
faulthandler_log_file:
+                    with open(
+                        faulthandler_log_path, "w", encoding="utf-8"
+                    ) as faulthandler_log_file:
                         faulthandler.dump_traceback(file=faulthandler_log_file)
                 raise
             else:
@@ -244,7 +246,7 @@ def manager() -> None:
                         # Therefore, here we redirects it to '/dev/null' by 
duplicating
                         # another file descriptor for '/dev/null' to the 
standard input (0).
                         # See SPARK-26175.
-                        devnull = open(os.devnull, "r")
+                        devnull = open(os.devnull, "r", encoding="utf-8")
                         os.dup2(devnull.fileno(), 0)
                         devnull.close()
 
diff --git a/python/pyspark/errors/error_classes.py 
b/python/pyspark/errors/error_classes.py
index 12094d61336e..4a840bafb8ae 100644
--- a/python/pyspark/errors/error_classes.py
+++ b/python/pyspark/errors/error_classes.py
@@ -23,6 +23,8 @@ import importlib.resources
 #   For more information, please see: 
https://issues.apache.org/jira/browse/SPARK-46810
 #   This discrepancy will be resolved as part of: 
https://issues.apache.org/jira/browse/SPARK-47429
 ERROR_CLASSES_JSON = (
-    
importlib.resources.files("pyspark.errors").joinpath("error-conditions.json").read_text()
+    importlib.resources.files("pyspark.errors")
+    .joinpath("error-conditions.json")
+    .read_text(encoding="utf-8")
 )
 ERROR_CLASSES_MAP = json.loads(ERROR_CLASSES_JSON)
diff --git a/python/pyspark/errors/exceptions/__init__.py 
b/python/pyspark/errors/exceptions/__init__.py
index 259073164853..3de00c17500c 100644
--- a/python/pyspark/errors/exceptions/__init__.py
+++ b/python/pyspark/errors/exceptions/__init__.py
@@ -23,7 +23,7 @@ def _write_self() -> None:
 
     ERRORS_DIR = Path(__file__).parents[1]
 
-    with open(ERRORS_DIR / "error-conditions.json", "w") as f:
+    with open(ERRORS_DIR / "error-conditions.json", "w", encoding="utf-8") as 
f:
         json.dump(
             error_classes.ERROR_CLASSES_MAP,
             f,
diff --git a/python/pyspark/errors_doc_gen.py b/python/pyspark/errors_doc_gen.py
index 53b8b8d1e12f..c541cd8fb7e2 100644
--- a/python/pyspark/errors_doc_gen.py
+++ b/python/pyspark/errors_doc_gen.py
@@ -45,7 +45,7 @@ This is a list of common, named error classes returned by 
PySpark which are defi
 
 When writing PySpark errors, developers must use an error class from the list. 
If an appropriate error class is not available, add a new one into the list. 
For more information, please refer to `Contributing Error and Exception 
<contributing.rst#contributing-error-and-exception>`_.
 """
-    with open(output_rst_file_path, "w") as f:
+    with open(output_rst_file_path, "w", encoding="utf-8") as f:
         f.write(header + "\n\n")
         for error_key, error_details in ERROR_CLASSES_MAP.items():
             f.write(error_key + "\n")
diff --git a/python/pyspark/ml/connect/io_utils.py 
b/python/pyspark/ml/connect/io_utils.py
index fcc2e8d93c0f..f6108934f399 100644
--- a/python/pyspark/ml/connect/io_utils.py
+++ b/python/pyspark/ml/connect/io_utils.py
@@ -131,7 +131,7 @@ class ParamsReadWrite(Params):
 
     def _save_to_local(self, path: str) -> None:
         metadata = self._save_to_node_path(path, [])
-        with open(os.path.join(path, _META_DATA_FILE_NAME), "w") as fp:
+        with open(os.path.join(path, _META_DATA_FILE_NAME), "w", 
encoding="utf-8") as fp:
             json.dump(metadata, fp)
 
     def saveToLocal(self, path: str, *, overwrite: bool = False) -> None:
@@ -195,7 +195,7 @@ class ParamsReadWrite(Params):
 
     @classmethod
     def _load_from_local(cls, path: str) -> "Params":
-        with open(os.path.join(path, _META_DATA_FILE_NAME), "r") as fp:
+        with open(os.path.join(path, _META_DATA_FILE_NAME), "r", 
encoding="utf-8") as fp:
             metadata = json.load(fp)
 
         return cls._load_instance_from_metadata(metadata, path)
diff --git a/python/pyspark/ml/deepspeed/deepspeed_distributor.py 
b/python/pyspark/ml/deepspeed/deepspeed_distributor.py
index 3fd1d3bb3246..162cd2adda52 100644
--- a/python/pyspark/ml/deepspeed/deepspeed_distributor.py
+++ b/python/pyspark/ml/deepspeed/deepspeed_distributor.py
@@ -100,7 +100,9 @@ class DeepspeedTorchDistributor(TorchDistributor):
     @staticmethod
     def _get_deepspeed_config_path(deepspeed_config: Union[str, Dict[str, 
Any]]) -> str:
         if isinstance(deepspeed_config, dict):
-            with tempfile.NamedTemporaryFile(mode="w+", delete=False, 
suffix=".json") as file:
+            with tempfile.NamedTemporaryFile(
+                mode="w+", delete=False, suffix=".json", encoding="utf-8"
+            ) as file:
                 json.dump(deepspeed_config, file)
                 return file.name
         deepspeed_config_path = deepspeed_config
diff --git a/python/pyspark/ml/dl_util.py b/python/pyspark/ml/dl_util.py
index 26538c328353..89cf2ea8f43c 100644
--- a/python/pyspark/ml/dl_util.py
+++ b/python/pyspark/ml/dl_util.py
@@ -119,7 +119,7 @@ class FunctionPickler:
                         with open("{fn_output_path}", "wb") as f:
                             cloudpickle.dump(output, f)
                     """)
-        with open(script_path, "w") as f:
+        with open(script_path, "w", encoding="utf-8") as f:
             if prefix_code != "":
                 f.write(prefix_code)
             f.write(code_snippet)
diff --git a/python/pyspark/ml/torch/distributor.py 
b/python/pyspark/ml/torch/distributor.py
index dc0eda962312..67ba6adb03c4 100644
--- a/python/pyspark/ml/torch/distributor.py
+++ b/python/pyspark/ml/torch/distributor.py
@@ -482,7 +482,7 @@ class TorchDistributor(Distributor):
         tail: collections.deque = collections.deque(maxlen=_TAIL_LINES_TO_KEEP)
         try:
             for line in task.stdout:  # type: ignore
-                decoded = line.decode()
+                decoded = line.decode("utf-8")
                 tail.append(decoded)
                 if redirect_to_stdout:
                     if (
@@ -883,7 +883,7 @@ class TorchDistributor(Distributor):
             schema_file_path = os.path.join(save_dir, "schema.json")
             schema_json_string = json.dumps(input_schema_json)
 
-            with open(schema_file_path, "w") as f:
+            with open(schema_file_path, "w", encoding="utf-8") as f:
                 f.write(schema_json_string)
 
             os.environ[SPARK_PARTITION_ARROW_DATA_FILE] = arrow_file_path
@@ -1088,7 +1088,7 @@ def _get_spark_partition_data_loader(
     arrow_file = os.environ[SPARK_PARTITION_ARROW_DATA_FILE]
     schema_file = os.environ[SPARK_DATAFRAME_SCHEMA_FILE]
 
-    with open(schema_file, "r") as fp:
+    with open(schema_file, "r", encoding="utf-8") as fp:
         schema = StructType.fromJson(json.load(fp))
 
     dataset = _SparkPartitionTorchDataset(arrow_file, schema, num_samples)
diff --git a/python/pyspark/ml/torch/torch_run_process_wrapper.py 
b/python/pyspark/ml/torch/torch_run_process_wrapper.py
index 67ec492329df..88415a2c8349 100644
--- a/python/pyspark/ml/torch/torch_run_process_wrapper.py
+++ b/python/pyspark/ml/torch/torch_run_process_wrapper.py
@@ -69,7 +69,7 @@ if __name__ == "__main__":
     task.stdin.close()  # type: ignore[union-attr]
     try:
         for line in task.stdout:  # type: ignore[union-attr]
-            decoded = line.decode()
+            decoded = line.decode("utf-8")
             print(decoded.rstrip())
         task.wait()
     finally:
diff --git a/python/pyspark/pandas/supported_api_gen.py 
b/python/pyspark/pandas/supported_api_gen.py
index c39b8880f14b..3a9c4f1d2468 100644
--- a/python/pyspark/pandas/supported_api_gen.py
+++ b/python/pyspark/pandas/supported_api_gen.py
@@ -419,7 +419,7 @@ def _write_rst(
     all_supported_status : Dict
         Collected support status data.
     """
-    with open(output_rst_file_path, "w") as w_fd:
+    with open(output_rst_file_path, "w", encoding="utf-8") as w_fd:
         w_fd.write(RST_HEADER)
         for module_info, supported_status in all_supported_status.items():
             module, module_path = module_info
diff --git a/python/pyspark/pipelines/cli.py b/python/pyspark/pipelines/cli.py
index d98e189611bd..9598987fc3f3 100644
--- a/python/pyspark/pipelines/cli.py
+++ b/python/pyspark/pipelines/cli.py
@@ -150,7 +150,7 @@ def find_pipeline_spec(current_dir: Path) -> Path:
 
 def load_pipeline_spec(spec_path: Path) -> PipelineSpec:
     """Load the pipeline spec from a YAML file at the given path."""
-    with spec_path.open("r") as f:
+    with spec_path.open("r", encoding="utf-8") as f:
         return unpack_pipeline_spec(yaml.safe_load(f))
 
 
@@ -261,7 +261,7 @@ def register_definitions(
                                 module_spec.loader.exec_module(module)
                     elif file.suffix == ".sql":
                         log_with_curr_timestamp(f"Registering SQL file 
{file}...")
-                        with file.open("r") as f:
+                        with file.open("r", encoding="utf-8") as f:
                             sql = f.read()
                         file_path_relative_to_spec = file.relative_to(path)
                         registry.register_sql(sql, file_path_relative_to_spec)
diff --git a/python/pyspark/pipelines/init_cli.py 
b/python/pyspark/pipelines/init_cli.py
index 18bbb70ed9c1..291aa44018ec 100644
--- a/python/pyspark/pipelines/init_cli.py
+++ b/python/pyspark/pipelines/init_cli.py
@@ -58,7 +58,7 @@ def init(name: str) -> None:
 
     # Write the spec file to the project directory
     spec_file = project_dir / "spark-pipeline.yml"
-    with open(spec_file, "w") as f:
+    with open(spec_file, "w", encoding="utf-8") as f:
         spec_content = SPEC.replace("{{ name }}", name).replace("{{ 
storage_root }}", storage_path)
         f.write(spec_content)
 
@@ -68,12 +68,12 @@ def init(name: str) -> None:
 
     # Create the Python example file
     python_example_file = transformations_dir / 
"example_python_materialized_view.py"
-    with open(python_example_file, "w") as f:
+    with open(python_example_file, "w", encoding="utf-8") as f:
         f.write(PYTHON_EXAMPLE)
 
     # Create the SQL example file
     sql_example_file = transformations_dir / 
"example_sql_materialized_view.sql"
-    with open(sql_example_file, "w") as f:
+    with open(sql_example_file, "w", encoding="utf-8") as f:
         f.write(SQL_EXAMPLE)
 
     print(f"Pipeline project '{name}' created successfully. To run your 
pipeline:")
diff --git a/python/pyspark/profiler.py b/python/pyspark/profiler.py
index 8f797b5f21fc..45ab1e5afdbf 100644
--- a/python/pyspark/profiler.py
+++ b/python/pyspark/profiler.py
@@ -410,7 +410,7 @@ class MemoryProfiler(Profiler):
         stats = self.stats()  # dict
         if stats:
             p = os.path.join(path, "udf_%d_memory.txt" % id)
-            with open(p, "w+") as f:
+            with open(p, "w+", encoding="utf-8") as f:
                 self._show_results(stats, stream=f)
 
 
diff --git a/python/pyspark/shell.py b/python/pyspark/shell.py
index dae4854a237e..435cc9de3b2e 100644
--- a/python/pyspark/shell.py
+++ b/python/pyspark/shell.py
@@ -138,6 +138,6 @@ print("SparkSession available as 'spark'.")
 # which allows us to execute the user's PYTHONSTARTUP file:
 _pythonstartup = os.environ.get("OLD_PYTHONSTARTUP")
 if _pythonstartup and os.path.isfile(_pythonstartup):
-    with open(_pythonstartup) as f:
+    with open(_pythonstartup, encoding="utf-8") as f:
         code = compile(f.read(), _pythonstartup, "exec")
         exec(code)
diff --git a/python/pyspark/shuffle.py b/python/pyspark/shuffle.py
index bfdc0b79ce41..c37bb7fbce30 100644
--- a/python/pyspark/shuffle.py
+++ b/python/pyspark/shuffle.py
@@ -80,7 +80,7 @@ def get_used_memory() -> int:
 
     else:
         if platform.system() == "Linux":
-            for line in open("/proc/self/status"):
+            for line in open("/proc/self/status", encoding="utf-8"):
                 if line.startswith("VmRSS:"):
                     return int(line.split()[1]) >> 10
 
diff --git a/python/pyspark/sql/profiler.py b/python/pyspark/sql/profiler.py
index 60f68c61708f..eae9b44f449b 100644
--- a/python/pyspark/sql/profiler.py
+++ b/python/pyspark/sql/profiler.py
@@ -394,7 +394,7 @@ class ProfilerCollector(ABC):
                 os.makedirs(path, exist_ok=True)
                 p = os.path.join(path, f"udf_{id}_memory.txt")
 
-                with open(p, "w+") as f:
+                with open(p, "w+", encoding="utf-8") as f:
                     MemoryProfiler._show_results(cm, stream=f)
 
         if id is not None:
diff --git a/python/pyspark/testing/sqlutils.py 
b/python/pyspark/testing/sqlutils.py
index e9d99dd3d2d6..1760ad5ffd79 100644
--- a/python/pyspark/testing/sqlutils.py
+++ b/python/pyspark/testing/sqlutils.py
@@ -84,6 +84,7 @@ def get_sbt_runtime_classpath(project_relative_path, 
project_name_map):
             cwd=SPARK_HOME,
             capture_output=True,
             text=True,
+            encoding="utf-8",
             timeout=180,
         )
 
@@ -138,7 +139,7 @@ def read_classpath(project_relative_path, 
project_name_map=None):
 
     # First try to read classpath.txt (Maven builds)
     if os.path.exists(classpath_file):
-        with open(classpath_file, "r") as f:
+        with open(classpath_file, "r", encoding="utf-8") as f:
             classpath = f.read().strip()
         # Replace colon with comma for spark-submit --jars format
         return classpath.replace(":", ",")
diff --git a/python/pyspark/util.py b/python/pyspark/util.py
index 0d1440962f7f..858dc1013e94 100644
--- a/python/pyspark/util.py
+++ b/python/pyspark/util.py
@@ -978,7 +978,7 @@ class _FaulthandlerHelper:
         self._log_path = os.environ.get("PYTHON_FAULTHANDLER_DIR", None)
         if self._log_path:
             self._log_path = os.path.join(self._log_path, str(os.getpid()))
-            self._log_file = open(self._log_path, "w")
+            self._log_file = open(self._log_path, "w", encoding="utf-8")
 
             faulthandler.enable(file=self._log_file)
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to