diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7bee8474d4db5..beedcd92c467d 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -313,7 +313,6 @@ jobs: # To freeze this file, uncomment out the ``if: false`` condition, and migrate the jobs # to the corresponding posix/windows-macos/sdist etc. workflows. # Feel free to modify this comment as necessary. - if: false defaults: run: shell: bash -eou pipefail {0} @@ -345,7 +344,7 @@ jobs: - name: Set up Python Dev Version uses: actions/setup-python@v5 with: - python-version: '3.13-dev' + python-version: '3.14-dev' - name: Build Environment run: | diff --git a/pandas/_testing/contexts.py b/pandas/_testing/contexts.py index da147c117ad43..b79cd797a3707 100644 --- a/pandas/_testing/contexts.py +++ b/pandas/_testing/contexts.py @@ -12,7 +12,10 @@ ) import uuid -from pandas.compat import PYPY +from pandas.compat import ( + PYPY, + WARNING_CHECK_BROKEN, +) from pandas.errors import ChainedAssignmentError from pandas.io.common import get_handle @@ -163,7 +166,7 @@ def with_csv_dialect(name: str, **kwargs) -> Generator[None]: def raises_chained_assignment_error(extra_warnings=(), extra_match=()): from pandas._testing import assert_produces_warning - if PYPY: + if PYPY or WARNING_CHECK_BROKEN: if not extra_warnings: from contextlib import nullcontext diff --git a/pandas/compat/__init__.py b/pandas/compat/__init__.py index d5dbcb74d29e4..be614c249fe87 100644 --- a/pandas/compat/__init__.py +++ b/pandas/compat/__init__.py @@ -20,7 +20,9 @@ ISMUSL, PY311, PY312, + PY314, PYPY, + WARNING_CHECK_BROKEN, WASM, ) from pandas.compat.numpy import is_numpy_dev @@ -157,7 +159,9 @@ def is_ci_environment() -> bool: "ISMUSL", "PY311", "PY312", + "PY314", "PYPY", + "WARNING_CHECK_BROKEN", "WASM", "is_numpy_dev", "pa_version_under12p1", diff --git a/pandas/compat/_constants.py b/pandas/compat/_constants.py index c7b7341013251..9f230dbeec97c 100644 --- a/pandas/compat/_constants.py +++ b/pandas/compat/_constants.py @@ -15,10 +15,13 @@ PY311 = sys.version_info >= (3, 11) PY312 = sys.version_info >= (3, 12) +PY314 = sys.version_info >= (3, 14) PYPY = platform.python_implementation() == "PyPy" WASM = (sys.platform == "emscripten") or (platform.machine() in ["wasm32", "wasm64"]) ISMUSL = "musl" in (sysconfig.get_config_var("HOST_GNU_TYPE") or "") REF_COUNT = 2 if PY311 else 3 +# hopefully there is a workaround in Python 3.14.1 +WARNING_CHECK_BROKEN = PY314 __all__ = [ "IS64", diff --git a/pandas/core/frame.py b/pandas/core/frame.py index e48620a854edb..3f1b8517fcc7f 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -49,7 +49,10 @@ from pandas._libs.hashtable import duplicated from pandas._libs.lib import is_range_indexer from pandas.compat import PYPY -from pandas.compat._constants import REF_COUNT +from pandas.compat._constants import ( + REF_COUNT, + WARNING_CHECK_BROKEN, +) from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import function as nv from pandas.errors import ( @@ -4296,8 +4299,8 @@ def __setitem__(self, key, value) -> None: z 3 50 # Values for 'a' and 'b' are completely ignored! """ - if not PYPY: - if sys.getrefcount(self) <= 3: + if not PYPY and not WARNING_CHECK_BROKEN: + if sys.getrefcount(self) <= REF_COUNT + 1: warnings.warn( _chained_assignment_msg, ChainedAssignmentError, stacklevel=2 ) @@ -9204,7 +9207,7 @@ def update( 1 2 500.0 2 3 6.0 """ - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 2ae28266266f6..de3e506866341 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -83,7 +83,10 @@ npt, ) from pandas.compat import PYPY -from pandas.compat._constants import REF_COUNT +from pandas.compat._constants import ( + REF_COUNT, + WARNING_CHECK_BROKEN, +) from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import function as nv from pandas.errors import ( @@ -7069,7 +7072,7 @@ def fillna( """ inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -7300,7 +7303,7 @@ def ffill( """ inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -7440,7 +7443,7 @@ def bfill( """ inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -7525,7 +7528,7 @@ def replace( inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -7888,7 +7891,7 @@ def interpolate( inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -8472,7 +8475,7 @@ def clip( inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -10081,7 +10084,7 @@ def where( """ inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, @@ -10145,7 +10148,7 @@ def mask( ) -> Self | None: inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 42dd8adbead09..6a796c92977af 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -15,6 +15,10 @@ from pandas._libs.indexing import NDFrameIndexerBase from pandas._libs.lib import item_from_zerodim from pandas.compat import PYPY +from pandas.compat._constants import ( + REF_COUNT, + WARNING_CHECK_BROKEN, +) from pandas.errors import ( AbstractMethodError, ChainedAssignmentError, @@ -913,8 +917,8 @@ def _ensure_listlike_indexer(self, key, axis=None, value=None) -> None: @final def __setitem__(self, key, value) -> None: - if not PYPY: - if sys.getrefcount(self.obj) <= 2: + if not PYPY and not WARNING_CHECK_BROKEN: + if sys.getrefcount(self.obj) <= REF_COUNT: warnings.warn( _chained_assignment_msg, ChainedAssignmentError, stacklevel=2 ) diff --git a/pandas/core/series.py b/pandas/core/series.py index 6ae03f2464f76..9242d47167f1c 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -34,7 +34,10 @@ ) from pandas._libs.lib import is_range_indexer from pandas.compat import PYPY -from pandas.compat._constants import REF_COUNT +from pandas.compat._constants import ( + REF_COUNT, + WARNING_CHECK_BROKEN, +) from pandas.compat._optional import import_optional_dependency from pandas.compat.numpy import function as nv from pandas.errors import ( @@ -1059,8 +1062,8 @@ def _get_value(self, label, takeable: bool = False): return self.iloc[loc] def __setitem__(self, key, value) -> None: - if not PYPY: - if sys.getrefcount(self) <= 3: + if not PYPY and not WARNING_CHECK_BROKEN: + if sys.getrefcount(self) <= REF_COUNT + 1: warnings.warn( _chained_assignment_msg, ChainedAssignmentError, stacklevel=2 ) @@ -3338,7 +3341,7 @@ def update(self, other: Series | Sequence | Mapping) -> None: 2 3 dtype: int64 """ - if not PYPY: + if not PYPY and not WARNING_CHECK_BROKEN: if sys.getrefcount(self) <= REF_COUNT: warnings.warn( _chained_assignment_method_msg, diff --git a/pandas/tests/copy_view/test_chained_assignment_deprecation.py b/pandas/tests/copy_view/test_chained_assignment_deprecation.py index 4aef69a6fde98..8aa7696c9275e 100644 --- a/pandas/tests/copy_view/test_chained_assignment_deprecation.py +++ b/pandas/tests/copy_view/test_chained_assignment_deprecation.py @@ -1,6 +1,7 @@ import numpy as np import pytest +from pandas.compat import WARNING_CHECK_BROKEN from pandas.errors import ChainedAssignmentError from pandas import DataFrame @@ -17,6 +18,8 @@ def test_series_setitem(indexer): # using custom check instead of tm.assert_produces_warning because that doesn't # fail if multiple warnings are raised + if WARNING_CHECK_BROKEN: + return with pytest.warns() as record: # noqa: TID251 df["a"][indexer] = 0 assert len(record) == 1 diff --git a/pandas/tests/frame/test_query_eval.py b/pandas/tests/frame/test_query_eval.py index f93105498ac79..b599be5d042fe 100644 --- a/pandas/tests/frame/test_query_eval.py +++ b/pandas/tests/frame/test_query_eval.py @@ -168,7 +168,7 @@ def test_query_duplicate_column_name(self, engine, parser): } ).rename(columns={"B": "A"}) - res = df.query('C == 1', engine=engine, parser=parser) + res = df.query("C == 1", engine=engine, parser=parser) expect = DataFrame( [[1, 1, 1]], @@ -1411,7 +1411,7 @@ def test_expr_with_column_name_with_backtick_and_hash(self): def test_expr_with_column_name_with_backtick(self): # GH 59285 df = DataFrame({"a`b": (1, 2, 3), "ab": (4, 5, 6)}) - result = df.query("`a``b` < 2") # noqa + result = df.query("`a``b` < 2") # Note: Formatting checks may wrongly consider the above ``inline code``. expected = df[df["a`b"] < 2] tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/indexes/test_indexing.py b/pandas/tests/indexes/test_indexing.py index 1bbffcee3b671..c1c9f0fde863d 100644 --- a/pandas/tests/indexes/test_indexing.py +++ b/pandas/tests/indexes/test_indexing.py @@ -18,6 +18,7 @@ import numpy as np import pytest +from pandas.compat import PY314 from pandas.errors import InvalidIndexError from pandas.core.dtypes.common import ( @@ -160,13 +161,19 @@ def test_contains_requires_hashable_raises(self, index): with pytest.raises(TypeError, match=msg): [] in index + if PY314: + container_or_iterable = "a container or iterable" + else: + container_or_iterable = "iterable" + msg = "|".join( [ r"unhashable type: 'dict'", r"must be real number, not dict", r"an integer is required", r"\{\}", - r"pandas\._libs\.interval\.IntervalTree' is not iterable", + r"pandas\._libs\.interval\.IntervalTree' is not " + f"{container_or_iterable}", ] ) with pytest.raises(TypeError, match=msg): diff --git a/pandas/tests/io/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py index ebc6ff5be108f..82f36c94801e3 100644 --- a/pandas/tests/io/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -1,4 +1,3 @@ -import codecs from datetime import datetime from textwrap import dedent @@ -42,7 +41,7 @@ def test_to_latex_to_file_utf8_with_encoding(self): df = DataFrame([["au\xdfgangen"]]) with tm.ensure_clean("test.tex") as path: df.to_latex(path, encoding="utf-8") - with codecs.open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: assert df.to_latex() == f.read() def test_to_latex_to_file_utf8_without_encoding(self): @@ -50,7 +49,7 @@ def test_to_latex_to_file_utf8_without_encoding(self): df = DataFrame([["au\xdfgangen"]]) with tm.ensure_clean("test.tex") as path: df.to_latex(path) - with codecs.open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: assert df.to_latex() == f.read() def test_to_latex_tabular_with_index(self): diff --git a/pandas/tests/io/parser/test_quoting.py b/pandas/tests/io/parser/test_quoting.py index a70b7e3389c1b..261003d94ddf0 100644 --- a/pandas/tests/io/parser/test_quoting.py +++ b/pandas/tests/io/parser/test_quoting.py @@ -8,7 +8,10 @@ import pytest -from pandas.compat import PY311 +from pandas.compat import ( + PY311, + PY314, +) from pandas.errors import ParserError from pandas import DataFrame @@ -21,15 +24,24 @@ skip_pyarrow = pytest.mark.usefixtures("pyarrow_skip") +if PY314: + # TODO: write a regex that works with all new possitibilities here + MSG1 = "" + MSG2 = r"[\s\S]*" +else: + MSG1 = "a(n)? 1-character string" + MSG2 = "string( or None)?" + + @pytest.mark.parametrize( "kwargs,msg", [ - ({"quotechar": "foo"}, '"quotechar" must be a(n)? 1-character string'), + ({"quotechar": "foo"}, f'"quotechar" must be {MSG1}'), ( {"quotechar": None, "quoting": csv.QUOTE_MINIMAL}, "quotechar must be set if quoting enabled", ), - ({"quotechar": 2}, '"quotechar" must be string( or None)?, not int'), + ({"quotechar": 2}, f'"quotechar" must be {MSG2}, not int'), ], ) @skip_pyarrow # ParserError: CSV parse error: Empty CSV file or block @@ -88,8 +100,12 @@ def test_null_quote_char(all_parsers, quoting, quote_char): if quoting != csv.QUOTE_NONE: # Sanity checking. + if not PY314: + msg = "1-character string" + else: + msg = "unicode character or None" msg = ( - '"quotechar" must be a 1-character string' + f'"quotechar" must be a {msg}' if PY311 and all_parsers.engine == "python" and quote_char == "" else "quotechar must be set if quoting enabled" ) diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 4a5e41397b59d..9f373f30c1817 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -513,9 +513,8 @@ def test_is_fsspec_url_chained(): assert not icom.is_fsspec_url("filecache::://pandas/test.csv") -@pytest.mark.parametrize("encoding", [None, "utf-8"]) @pytest.mark.parametrize("format", ["csv", "json"]) -def test_codecs_encoding(encoding, format): +def test_codecs_encoding(format): # GH39247 expected = pd.DataFrame( 1.1 * np.arange(120).reshape((30, 4)), @@ -523,9 +522,9 @@ def test_codecs_encoding(encoding, format): index=pd.Index([f"i-{i}" for i in range(30)]), ) with tm.ensure_clean() as path: - with codecs.open(path, mode="w", encoding=encoding) as handle: + with open(path, mode="w", encoding="utf-8") as handle: getattr(expected, f"to_{format}")(handle) - with codecs.open(path, mode="r", encoding=encoding) as handle: + with open(path, encoding='utf-8') as handle: if format == "csv": df = pd.read_csv(handle, index_col=0) else: diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py index a8e29ef03acc2..2d16c0fe452e0 100644 --- a/pandas/tests/reshape/merge/test_merge.py +++ b/pandas/tests/reshape/merge/test_merge.py @@ -8,6 +8,8 @@ import numpy as np import pytest +from pandas.compat import PY314 + from pandas.core.dtypes.common import ( is_object_dtype, is_string_dtype, @@ -2420,10 +2422,18 @@ def test_merge_suffix_raises(suffixes): merge(a, b, left_index=True, right_index=True, suffixes=suffixes) +TWO_GOT_THREE = "2, got 3" if PY314 else "2" + + @pytest.mark.parametrize( "col1, col2, suffixes, msg", [ - ("a", "a", ("a", "b", "c"), r"too many values to unpack \(expected 2\)"), + ( + "a", + "a", + ("a", "b", "c"), + (rf"too many values to unpack \(expected {TWO_GOT_THREE}\)"), + ), ("a", "a", tuple("a"), r"not enough values to unpack \(expected 2, got 1\)"), ], ) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index baaedaa853565..d8404937dcf97 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -16,6 +16,7 @@ from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime from pandas._libs.tslibs.parsing import DateParseError from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG +from pandas.compat import PY314 from pandas import ( NaT, @@ -344,7 +345,10 @@ def test_invalid_arguments(self): msg = '^Given date string "-2000" not likely a datetime$' with pytest.raises(ValueError, match=msg): Period("-2000", "Y") - msg = "day is out of range for month" + if PY314: + msg = "day 0 must be in range 1..31 for month 1 in year 1: 0" + else: + msg = "day is out of range for month" with pytest.raises(DateParseError, match=msg): Period("0", "Y") msg = "Unknown datetime string format, unable to parse" diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py index 09ca5a71503ad..dcb5fb94511e9 100644 --- a/pandas/tests/scalar/timestamp/test_constructors.py +++ b/pandas/tests/scalar/timestamp/test_constructors.py @@ -17,6 +17,7 @@ import pytest from pandas._libs.tslibs.dtypes import NpyDatetimeUnit +from pandas.compat import PY314 from pandas.errors import OutOfBoundsDatetime from pandas import ( @@ -212,13 +213,19 @@ def test_constructor_positional(self): with pytest.raises(TypeError, match=msg): Timestamp(2000, 1) - msg = "month must be in 1..12" + if PY314: + msg = "month must be in 1..12, not" + else: + msg = "day is out of range for month" with pytest.raises(ValueError, match=msg): Timestamp(2000, 0, 1) with pytest.raises(ValueError, match=msg): Timestamp(2000, 13, 1) - msg = "day is out of range for month" + if PY314: + msg = "must be in range 1..31 for month 1 in year 2000" + else: + msg = "day is out of range for month" with pytest.raises(ValueError, match=msg): Timestamp(2000, 1, 0) with pytest.raises(ValueError, match=msg): @@ -242,7 +249,10 @@ def test_constructor_keyword(self): with pytest.raises(ValueError, match=msg): Timestamp(year=2000, month=13, day=1) - msg = "day is out of range for month" + if PY314: + msg = "must be in range 1..31 for month 1 in year 2000" + else: + msg = "day is out of range for month" with pytest.raises(ValueError, match=msg): Timestamp(year=2000, month=1, day=0) with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/tools/test_to_datetime.py b/pandas/tests/tools/test_to_datetime.py index 9bc88a7e0a824..3df686e2c6d1a 100644 --- a/pandas/tests/tools/test_to_datetime.py +++ b/pandas/tests/tools/test_to_datetime.py @@ -21,7 +21,10 @@ iNaT, parsing, ) -from pandas.compat import WASM +from pandas.compat import ( + PY314, + WASM, +) from pandas.errors import ( OutOfBoundsDatetime, OutOfBoundsTimedelta, @@ -57,6 +60,16 @@ r"alongside this." ) +if PY314: + NOT_99 = ", not 99" + DAY_IS_OUT_OF_RANGE = ( + r"day \d{1,2} must be in range 1\.\.\d{1,2} for " + r"month \d{1,2} in year \d{4}" + ) +else: + NOT_99 = "" + DAY_IS_OUT_OF_RANGE = "day is out of range for month" + class TestTimeConversionFormats: def test_to_datetime_readonly(self, writable): @@ -1372,7 +1385,7 @@ def test_datetime_invalid_scalar(self, value, format): r'^Given date string "a" not likely a datetime$', r'^unconverted data remains when parsing with format "%H:%M:%S": "9". ' f"{PARSING_ERR_MSG}$", - r"^second must be in 0..59: 00:01:99$", + rf"^second must be in 0..59{NOT_99}: 00:01:99$", ] ) with pytest.raises(ValueError, match=msg): @@ -1424,7 +1437,7 @@ def test_datetime_invalid_index(self, values, format): f"{PARSING_ERR_MSG}$", r'^unconverted data remains when parsing with format "%H:%M:%S": "9". ' f"{PARSING_ERR_MSG}$", - r"^second must be in 0..59: 00:01:99$", + rf"^second must be in 0..59{NOT_99}: 00:01:99$", ] ) with pytest.raises(ValueError, match=msg): @@ -2851,7 +2864,10 @@ def test_day_not_in_month_coerce(self, cache, arg, format): assert isna(to_datetime(arg, errors="coerce", format=format, cache=cache)) def test_day_not_in_month_raise(self, cache): - msg = "day is out of range for month: 2015-02-29" + if PY314: + msg = "day 29 must be in range 1..28 for month 2 in year 2015: 2015-02-29" + else: + msg = "day is out of range for month: 2015-02-29" with pytest.raises(ValueError, match=msg): to_datetime("2015-02-29", errors="raise", cache=cache) @@ -2861,12 +2877,12 @@ def test_day_not_in_month_raise(self, cache): ( "2015-02-29", "%Y-%m-%d", - f"^day is out of range for month. {PARSING_ERR_MSG}$", + f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$", ), ( "2015-29-02", "%Y-%d-%m", - f"^day is out of range for month. {PARSING_ERR_MSG}$", + f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$", ), ( "2015-02-32", @@ -2883,12 +2899,12 @@ def test_day_not_in_month_raise(self, cache): ( "2015-04-31", "%Y-%m-%d", - f"^day is out of range for month. {PARSING_ERR_MSG}$", + f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$", ), ( "2015-31-04", "%Y-%d-%m", - f"^day is out of range for month. {PARSING_ERR_MSG}$", + f"^{DAY_IS_OUT_OF_RANGE}. {PARSING_ERR_MSG}$", ), ], ) diff --git a/pandas/util/_print_versions.py b/pandas/util/_print_versions.py index bd20660bdbba6..efcb50bf4345c 100644 --- a/pandas/util/_print_versions.py +++ b/pandas/util/_print_versions.py @@ -1,6 +1,5 @@ from __future__ import annotations -import codecs import json import locale import os @@ -143,7 +142,7 @@ def show_versions(as_json: str | bool = False) -> None: sys.stdout.writelines(json.dumps(j, indent=2)) else: assert isinstance(as_json, str) # needed for mypy - with codecs.open(as_json, "wb", encoding="utf8") as f: + with open(as_json, "w", encoding="utf-8") as f: json.dump(j, f, indent=2) else: