Repository: modin-project/modin (2025-05-19 ~ 2025-05-30)

problem_statement: BUG: Ambiguous error message when accessing missing attributes
### Modin version checks

- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest released version of Modin.

- [x] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-main-branch).)


### Reproducible Example

```python
import modin.pandas as pd
pd.DataFrame().x
```

### Issue Description

The above example raises `AttributeError: 'super' object has no attribute '__getattr__'. Did you mean: '__setattr__'?`.

This occurs because of this line:
https://github.com/modin-project/modin/blob/f2002454c8b2db80fb372fb463643ff507711c19/modin/pandas/dataframe.py#L2651

`BasePandasDataset` does not define `__getattr__` (likely changed at some point in the past few PRs), so this code path should probably just check `key in self.columns` before raising a new `AttributeError`.

### Expected Behavior

Error message should match pandas more closely (in this case, `AttributeError: 'DataFrame' object has no attribute 'x'. Did you mean: 'xs'?`).

### Error Logs

<details>

```python-traceback

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/joshi/code/modin/modin/pandas/dataframe.py", line 2656, in __getattr__
    raise err
  File "/Users/joshi/code/modin/modin/pandas/dataframe.py", line 2652, in __getattr__
    return super().__getattr__(key)
           ^^^^^^^^^^^^^^^^^^^
AttributeError: 'super' object has no attribute '__getattr__'. Did you mean: '__setattr__'?
```

</details>


### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit                : f2002454c8b2db80fb372fb463643ff507711c19
python                : 3.11.11
python-bits           : 64
OS                    : Darwin
OS-release            : 24.5.0
Version               : Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:25 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6020
machine               : arm64
processor             : arm
byteorder             : little
LC_ALL                : None
LANG                  : en_US.UTF-8
LOCALE                : en_US.UTF-8

Modin dependencies
------------------
modin                 : 0.32.0+66.gf2002454.dirty
ray                   : 2.44.0
dask                  : 2025.3.0
distributed           : 2025.3.0

pandas dependencies
-------------------
pandas                : 2.2.3
numpy                 : 1.26.4
pytz                  : 2024.2
dateutil              : 2.9.0.post0
pip                   : 24.2
Cython                : None
sphinx                : 5.0.2
IPython               : 9.0.2
adbc-driver-postgresql: None
adbc-driver-sqlite    : None
bs4                   : 4.13.3
blosc                 : None
bottleneck            : None
dataframe-api-compat  : None
fastparquet           : None
fsspec                : 2024.12.0
html5lib              : None
hypothesis            : None
gcsfs                 : None
jinja2                : 3.1.5
lxml.etree            : 5.3.0
matplotlib            : 3.10.0
numba                 : None
numexpr               : None
odfpy                 : None
openpyxl              : 3.1.5
pandas_gbq            : None
psycopg2              : None
pymysql               : None
pyarrow               : 18.1.0
pyreadstat            : None
pytest                : 7.4.4
python-calamine       : None
pyxlsb                : None
s3fs                  : 2024.12.0
scipy                 : 1.15.1
sqlalchemy            : None
tables                : None
tabulate              : None
xarray                : None
xlrd                  : None
xlsxwriter            : None
zstandard             : None
tzdata                : 2024.2
qtpy                  : None
pyqt5                 : None
</details>

date: 2025-05-19
patch: diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
index c2df94d9..2c28796a 100644
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -2647,12 +2647,9 @@ class DataFrame(BasePandasDataset):
         """
         # NOTE that to get an attribute, python calls __getattribute__() first and
         # then falls back to __getattr__() if the former raises an AttributeError.
-        try:
-            return super().__getattr__(key)
-        except AttributeError as err:
-            if key not in _ATTRS_NO_LOOKUP and key in self.columns:
-                return self[key]
-            raise err
+        if key not in _ATTRS_NO_LOOKUP and key in self.columns:
+            return self[key]
+        raise AttributeError(f"'DataFrame' object has no attribute '{key}'")
 
     def __setattr__(self, key, value) -> None:
         """
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
index 01e61b62..94b27c54 100644
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -390,12 +390,9 @@ class Series(BasePandasDataset):
         """
         # NOTE that to get an attribute, python calls __getattribute__() first and
         # then falls back to __getattr__() if the former raises an AttributeError.
-        try:
-            return super().__getattr__(key)
-        except AttributeError as err:
-            if key not in _ATTRS_NO_LOOKUP and key in self._query_compiler.index:
-                return self[key]
-            raise err
+        if key not in _ATTRS_NO_LOOKUP and key in self._query_compiler.index:
+            return self[key]
+        raise AttributeError(f"'Series' object has no attribute '{key}'")
 
     __float__ = _coerce_method(float)
     __int__ = _coerce_method(int)

test_patch: diff --git a/modin/tests/pandas/extensions/test_dataframe_extensions.py b/modin/tests/pandas/extensions/test_dataframe_extensions.py
index fc6f7f6f..88ae0266 100644
--- a/modin/tests/pandas/extensions/test_dataframe_extensions.py
+++ b/modin/tests/pandas/extensions/test_dataframe_extensions.py
@@ -224,7 +224,10 @@ class TestProperty:
 
         df = pd.DataFrame({column_name: ["a"]}).set_backend(Backend1)
 
-        with pytest.raises(AttributeError):
+        with pytest.raises(
+            AttributeError,
+            match="'DataFrame' object has no attribute 'non_existent_column'",
+        ):
             getattr(df, "non_existent_column")
         column_getter.assert_called_once_with(df)
 


----

problem_statement: BUG: Using extensions to override a cached_property causes an error
### Modin version checks

- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest released version of Modin.

- [x] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-main-branch).)


### Reproducible Example

```python
import modin.pandas as pd
from modin.pandas.api.extensions import (
    register_dataframe_groupby_accessor,
)
from functools import cached_property

@register_dataframe_groupby_accessor("groups")
@cached_property
def groups(self):
    return {0: pd.Index([0])}

pd.DataFrame([1]).groupby(0).groups()
```

### Issue Description

The above code raises `TypeError: Cannot use cached_property instance without calling __set_name__ on it.`

When `functools.cached_property` outside of a class definition, it seems to be unable to identify the class it should be bound to. Since `cached_property` uses the object's `__dict__` as a cache, it requires `__set_name__` to be properly called first.

This issue is similar to this SO post: https://stackoverflow.com/questions/62160411/pythons-new-functools-cached-property-bug-or-limitation

### Expected Behavior

Should not error.

### Error Logs

<details>

```python-traceback

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/joshi/code/modin/modin/pandas/groupby.py", line 279, in __getattribute__
    extensions_result = self._getattribute__from_extension_impl(
  File "/Users/joshi/code/modin/modin/core/storage_formats/pandas/query_compiler_caster.py", line 344, in _getattribute__from_extension_impl
    extension.__get__(self) if hasattr(extension, "__get__") else extension
  File "/Users/joshi/miniconda3/envs/modin-dev/lib/python3.10/functools.py", line 965, in __get__
    raise TypeError(
TypeError: Cannot use cached_property instance without calling __set_name__ on it.

```

</details>


### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit                : 5bc025e5303196b4bf7a0dc41da53d141969feae
python                : 3.10.13.final.0
python-bits           : 64
OS                    : Darwin
OS-release            : 24.5.0
Version               : Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:25 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6020
machine               : arm64
processor             : arm
byteorder             : little
LC_ALL                : None
LANG                  : en_US.UTF-8
LOCALE                : en_US.UTF-8

Modin dependencies
------------------
modin                 : 0.32.0+67.g5bc025e5
ray                   : 2.34.0
dask                  : 2024.8.1
distributed           : 2024.8.1

pandas dependencies
-------------------
pandas                : 2.2.2
numpy                 : 1.26.4
pytz                  : 2023.3.post1
dateutil              : 2.8.2
setuptools            : 68.0.0
pip                   : 23.3
Cython                : None
pytest                : 8.3.2
hypothesis            : None
sphinx                : 5.3.0
blosc                 : None
feather               : None
xlsxwriter            : None
lxml.etree            : 5.3.0
html5lib              : None
pymysql               : None
psycopg2              : 2.9.9
jinja2                : 3.1.4
IPython               : 8.17.2
pandas_datareader     : None
adbc-driver-postgresql: None
adbc-driver-sqlite    : None
bs4                   : 4.12.2
bottleneck            : None
dataframe-api-compat  : None
fastparquet           : 2024.5.0
fsspec                : 2024.6.1
gcsfs                 : None
matplotlib            : 3.9.2
numba                 : None
numexpr               : 2.10.1
odfpy                 : None
openpyxl              : 3.1.5
pandas_gbq            : 0.23.1
pyarrow               : 17.0.0
pyreadstat            : None
python-calamine       : None
pyxlsb                : None
s3fs                  : 2024.6.1
scipy                 : 1.14.1
sqlalchemy            : 2.0.32
tables                : 3.10.1
tabulate              : 0.9.0
xarray                : 2024.7.0
xlrd                  : 2.0.1
zstandard             : None
tzdata                : 2023.3
qtpy                  : None
pyqt5                 : None
</details>

date: 2025-05-22
patch: diff --git a/modin/pandas/api/extensions/extensions.py b/modin/pandas/api/extensions/extensions.py
index fa97f1c5..c8cfef5a 100644
--- a/modin/pandas/api/extensions/extensions.py
+++ b/modin/pandas/api/extensions/extensions.py
@@ -13,6 +13,7 @@
 
 import inspect
 from collections import defaultdict
+from functools import cached_property
 from types import MethodType, ModuleType
 from typing import Any, Dict, Optional, Union
 
@@ -94,6 +95,10 @@ def _set_attribute_on_obj(
             original_attr = getattr(pd, name)
             _reexport_classes[name] = original_attr
             delattr(pd, name)
+        # If the attribute is an instance of functools.cached_property, we must manually call __set_name__ on it.
+        # https://stackoverflow.com/a/62161136
+        if isinstance(new_attr, cached_property):
+            new_attr.__set_name__(obj, name)
         extensions[None if backend is None else Backend.normalize(backend)][
             name
         ] = new_attr
diff --git a/modin/pandas/groupby.py b/modin/pandas/groupby.py
index 456fbd41..975c8fd4 100644
--- a/modin/pandas/groupby.py
+++ b/modin/pandas/groupby.py
@@ -91,6 +91,21 @@ _DEFAULT_BEHAVIOUR = EXTENSION_NO_LOOKUP | {
     "_wrap_aggregation",
 }
 
+GROUPBY_EXTENSION_NO_LOOKUP = EXTENSION_NO_LOOKUP | {
+    "_axis",
+    "_idx_name",
+    "_df",
+    "_query_compiler",
+    "_columns",
+    "_by",
+    "_drop",
+    "_return_tuple_when_iterating",
+    "_is_multi_by",
+    "_level",
+    "_kwargs",
+    "_get_query_compiler",
+}
+
 
 @_inherit_docstrings(pandas.core.groupby.DataFrameGroupBy)
 class DataFrameGroupBy(ClassLogger, QueryCompilerCaster):  # noqa: GL08
@@ -98,10 +113,6 @@ class DataFrameGroupBy(ClassLogger, QueryCompilerCaster):  # noqa: GL08
     _return_tuple_when_iterating = False
     _df: Union[DataFrame, Series]
     _query_compiler: BaseQueryCompiler
-    # TODO(https://github.com/modin-project/modin/issues/7543):
-    # Currently this _extensions dict doesn't do anything, but we should
-    # add methods to register groupby accessors and make the groupby classes
-    # use this _extensions dict.
     _extensions: EXTENSION_DICT_TYPE = EXTENSION_DICT_TYPE(dict)
 
     def __init__(
@@ -248,7 +259,7 @@ class DataFrameGroupBy(ClassLogger, QueryCompilerCaster):  # noqa: GL08
         try:
             return self._getattr__from_extension_impl(
                 key=key,
-                default_behavior_attributes=_DEFAULT_BEHAVIOUR,
+                default_behavior_attributes=GROUPBY_EXTENSION_NO_LOOKUP,
                 extensions=__class__._extensions,
             )
         except AttributeError as err:
@@ -275,7 +286,7 @@ class DataFrameGroupBy(ClassLogger, QueryCompilerCaster):  # noqa: GL08
         Any
             The value of the attribute.
         """
-        if item not in _DEFAULT_BEHAVIOUR:
+        if item not in GROUPBY_EXTENSION_NO_LOOKUP:
             extensions_result = self._getattribute__from_extension_impl(
                 item, __class__._extensions
             )
@@ -1863,11 +1874,6 @@ class DataFrameGroupBy(ClassLogger, QueryCompilerCaster):  # noqa: GL08
 @_inherit_docstrings(pandas.core.groupby.SeriesGroupBy)
 class SeriesGroupBy(DataFrameGroupBy):  # noqa: GL08
     _pandas_class = pandas.core.groupby.SeriesGroupBy
-
-    # TODO(https://github.com/modin-project/modin/issues/7543):
-    # Currently this _extensions dict doesn't do anything, but we should
-    # add methods to register groupby accessors and make the groupby classes
-    # use this _extensions dict.
     _extensions: EXTENSION_DICT_TYPE = EXTENSION_DICT_TYPE(dict)
 
     @disable_logging
@@ -1888,7 +1894,7 @@ class SeriesGroupBy(DataFrameGroupBy):  # noqa: GL08
         Any
             The value of the attribute.
         """
-        if item not in _DEFAULT_BEHAVIOUR:
+        if item not in GROUPBY_EXTENSION_NO_LOOKUP:
             extensions_result = self._getattribute__from_extension_impl(
                 item, __class__._extensions
             )
@@ -1901,7 +1907,7 @@ class SeriesGroupBy(DataFrameGroupBy):  # noqa: GL08
     def __getattr__(self, key: str) -> Any:
         return self._getattr__from_extension_impl(
             key=key,
-            default_behavior_attributes=_DEFAULT_BEHAVIOUR,
+            default_behavior_attributes=GROUPBY_EXTENSION_NO_LOOKUP,
             extensions=__class__._extensions,
         )
 

test_patch: diff --git a/modin/tests/pandas/extensions/test_groupby_extensions.py b/modin/tests/pandas/extensions/test_groupby_extensions.py
index 6eb3f87d..8be6c69c 100644
--- a/modin/tests/pandas/extensions/test_groupby_extensions.py
+++ b/modin/tests/pandas/extensions/test_groupby_extensions.py
@@ -11,6 +11,8 @@
 # ANY KIND, either express or implied. See the License for the specific language
 # governing permissions and limitations under the License.
 
+from functools import cached_property
+
 import pytest
 
 import modin.pandas as pd
@@ -241,6 +243,16 @@ class TestProperty:
         delattr(pandas_groupby, public_property_name)
         assert not hasattr(pandas_groupby, private_property_name)
 
+    @pytest.mark.filterwarnings(default_to_pandas_ignore_string)
+    def test_override_cached_property(self, get_groupby, register_accessor):
+        @cached_property
+        def groups(self):
+            return {"group": pd.Index(["test"])}
+
+        register_accessor("groups", backend="Pandas")(groups)
+        pandas_df = pd.DataFrame({"col0": [1], "col1": [2]}).move_to("pandas")
+        assert get_groupby(pandas_df).groups == {"group": pd.Index(["test"])}
+
 
 def test_deleting_extension_that_is_not_property_raises_attribute_error():
     expected_string_val = "Some string value"


----

problem_statement: automatic switching debug logs use root logger instead of modin logger.
We should use [modin’s get_logger()](https://github.com/modin-project/modin/blob/5d7261626483b5d52e7fee834816c43349e9a39d/modin/logging/config.py#L220) instead of [logging.info](https://github.com/modin-project/modin/blob/5d7261626483b5d52e7fee834816c43349e9a39d/modin/core/storage_formats/pandas/query_compiler_caster.py#L645-L655).
date: 2025-05-30
patch: diff --git a/modin/core/storage_formats/base/query_compiler_calculator.py b/modin/core/storage_formats/base/query_compiler_calculator.py
index 5b15274f..2ba27ff6 100644
--- a/modin/core/storage_formats/base/query_compiler_calculator.py
+++ b/modin/core/storage_formats/base/query_compiler_calculator.py
@@ -19,7 +19,6 @@ between a set of different backends. It aggregates the cost across
 all query compilers to determine the best query compiler to use.
 """
 
-import logging
 from types import MappingProxyType
 from typing import Any, Optional
 
@@ -27,6 +26,7 @@ from modin.core.storage_formats.base.query_compiler import (
     BaseQueryCompiler,
     QCCoercionCost,
 )
+from modin.logging import get_logger
 
 
 class AggregatedBackendData:
@@ -143,7 +143,7 @@ class BackendCostCalculator:
                 self._result_backend = k
 
         if len(self._backend_data) > 1:
-            logging.info(
+            get_logger().info(
                 f"BackendCostCalculator Results: {self._calc_result_log(self._result_backend)}"
             )
 
diff --git a/modin/core/storage_formats/pandas/query_compiler_caster.py b/modin/core/storage_formats/pandas/query_compiler_caster.py
index a9a7844f..a269fecd 100644
--- a/modin/core/storage_formats/pandas/query_compiler_caster.py
+++ b/modin/core/storage_formats/pandas/query_compiler_caster.py
@@ -21,7 +21,6 @@ This ensures compatibility between different query compiler classes.
 
 import functools
 import inspect
-import logging
 from abc import ABC, abstractmethod
 from collections import defaultdict, namedtuple
 from types import FunctionType, MappingProxyType, MethodType
@@ -41,7 +40,7 @@ from modin.core.storage_formats.base.query_compiler_calculator import (
     BackendCostCalculator,
 )
 from modin.error_message import ErrorMessage
-from modin.logging import disable_logging
+from modin.logging import disable_logging, get_logger
 from modin.utils import sentinel
 
 Fn = TypeVar("Fn", bound=Any)
@@ -778,7 +777,7 @@ def _get_backend_for_auto_switch(
             ):
                 min_move_stay_delta = move_stay_delta
                 best_backend = backend
-            logging.info(
+            get_logger().info(
                 f"After {class_of_wrapped_fn} function {function_name}, "
                 + f"considered moving to backend {backend} with "
                 + f"(transfer_cost {move_to_cost} + other_execution_cost {other_execute_cost}) "
@@ -786,9 +785,11 @@ def _get_backend_for_auto_switch(
                 + f"{move_stay_delta}"
             )
     if best_backend == starting_backend:
-        logging.info(f"Chose not to switch backends after operation {function_name}")
+        get_logger().info(
+            f"Chose not to switch backends after operation {function_name}"
+        )
     else:
-        logging.info(f"Chose to move to backend {best_backend}")
+        get_logger().info(f"Chose to move to backend {best_backend}")
     return best_backend
 
 
diff --git a/modin/logging/__init__.py b/modin/logging/__init__.py
index 8e39a00c..530aa1cd 100644
--- a/modin/logging/__init__.py
+++ b/modin/logging/__init__.py
@@ -12,7 +12,7 @@
 # governing permissions and limitations under the License.
 
 from .class_logger import ClassLogger  # noqa: F401
-from .config import get_logger  # noqa: F401
+from .config import DEFAULT_LOGGER_NAME, get_logger  # noqa: F401
 from .logger_decorator import disable_logging, enable_logging  # noqa: F401
 from .metrics import add_metric_handler, clear_metric_handler, emit_metric
 
@@ -24,4 +24,5 @@ __all__ = [
     "emit_metric",
     "add_metric_handler",
     "clear_metric_handler",
+    "DEFAULT_LOGGER_NAME",
 ]
diff --git a/modin/logging/config.py b/modin/logging/config.py
index 9d4c20b3..9837ac05 100644
--- a/modin/logging/config.py
+++ b/modin/logging/config.py
@@ -34,6 +34,8 @@ import psutil
 import modin
 from modin.config import LogFileSize, LogMemoryInterval, LogMode
 
+DEFAULT_LOGGER_NAME = "modin.logger.default"
+
 __LOGGER_CONFIGURED__: bool = False
 
 
@@ -166,7 +168,7 @@ def configure_logging() -> None:
     job_id = f"{current_timestamp}_{uuid.uuid4().hex}"
 
     logger = _create_logger(
-        "modin.logger.default",
+        DEFAULT_LOGGER_NAME,
         job_id,
         "trace",
         LogLevel.INFO,

test_patch: diff --git a/modin/conftest.py b/modin/conftest.py
index ac52db06..1fc003ea 100644
--- a/modin/conftest.py
+++ b/modin/conftest.py
@@ -15,6 +15,7 @@
 # type: ignore
 
 import copy
+import logging
 import os
 import platform
 import shutil
@@ -792,3 +793,17 @@ def clean_up_auto_backend_switching():
         )
     ):
         yield
+
+
+@pytest.fixture(autouse=True)
+def assert_no_root_logging(caplog):
+    root_logger = logging.getLogger()
+    # Capture logs at any level, i.e. at level >= logging.NOTSET.
+    with caplog.at_level(logging.NOTSET):
+        yield
+    # Note that because this code is in a fixture, we have to use
+    # caplog.get_records(when="call") instead of caplog.records:
+    # https://github.com/pytest-dev/pytest/issues/4033
+    assert not any(
+        record.name == root_logger.name for record in caplog.get_records(when="call")
+    )
diff --git a/modin/tests/pandas/native_df_interoperability/test_compiler_caster.py b/modin/tests/pandas/native_df_interoperability/test_compiler_caster.py
index c5bb52d5..9bb102b6 100644
--- a/modin/tests/pandas/native_df_interoperability/test_compiler_caster.py
+++ b/modin/tests/pandas/native_df_interoperability/test_compiler_caster.py
@@ -13,6 +13,7 @@
 
 import contextlib
 import json
+import logging
 from io import StringIO
 from types import MappingProxyType
 from typing import Iterator
@@ -44,6 +45,7 @@ from modin.core.storage_formats.pandas.query_compiler_caster import (
     register_function_for_post_op_switch,
     register_function_for_pre_op_switch,
 )
+from modin.logging import DEFAULT_LOGGER_NAME
 from modin.pandas.api.extensions import register_pd_accessor
 from modin.tests.pandas.utils import create_test_dfs, df_equals, eval_general
 
@@ -368,12 +370,19 @@ def test_two_same_backend(pico_df):
     assert df3.get_backend() == "Pico"
 
 
-def test_cast_to_second_backend_with_concat(pico_df, cluster_df):
-    df3 = pd.concat([pico_df, cluster_df], axis=1)
+def test_cast_to_second_backend_with_concat(pico_df, cluster_df, caplog):
+    with caplog.at_level(level=logging.INFO, logger=DEFAULT_LOGGER_NAME):
+        df3 = pd.concat([pico_df, cluster_df], axis=1)
     assert pico_df.get_backend() == "Pico"
     assert cluster_df.get_backend() == "Cluster"
     assert df3.get_backend() == "Cluster"  # result should be on cluster
 
+    log_records = caplog.records
+    assert len(log_records) == 1
+    assert log_records[0].name == DEFAULT_LOGGER_NAME
+    assert log_records[0].levelno == logging.INFO
+    assert log_records[0].message.startswith("BackendCostCalculator Results: ")
+
 
 def test_cast_to_second_backend_with_concat_uses_second_backend_api_override(
     pico_df, cluster_df
@@ -692,6 +701,72 @@ class TestSwitchBackendPostOpDependingOnDataSize:
                 pd.read_json(StringIO(small_json)).get_backend() == "Small_Data_Local"
             )
 
+    @backend_test_context(
+        test_backend="Big_Data_Cloud",
+        choices=("Big_Data_Cloud", "Small_Data_Local"),
+    )
+    def test_read_json_logging_for_post_op_switch(self, caplog):
+        register_function_for_post_op_switch(
+            class_name=None, backend="Big_Data_Cloud", method="read_json"
+        )
+        with caplog.at_level(level=logging.INFO, logger=DEFAULT_LOGGER_NAME):
+            assert (
+                pd.read_json(
+                    StringIO(
+                        json.dumps(
+                            {"col0": list(range(BIG_DATA_CLOUD_MIN_NUM_ROWS - 1))}
+                        )
+                    )
+                ).get_backend()
+                == "Small_Data_Local"
+            )
+        log_records = caplog.records
+        assert len(log_records) == 2
+
+        assert log_records[0].name == DEFAULT_LOGGER_NAME
+        assert log_records[0].levelno == logging.INFO
+        assert log_records[0].message.startswith(
+            "After None function read_json, considered moving to backend Small_Data_Local with"
+        )
+
+        assert log_records[1].name == DEFAULT_LOGGER_NAME
+        assert log_records[1].levelno == logging.INFO
+        assert log_records[1].message.startswith(
+            "Chose to move to backend Small_Data_Local"
+        )
+
+    @backend_test_context(
+        test_backend="Big_Data_Cloud",
+        choices=("Big_Data_Cloud", "Small_Data_Local"),
+    )
+    def test_read_json_logging_for_post_op_not_switch(self, caplog):
+        register_function_for_post_op_switch(
+            class_name=None, backend="Big_Data_Cloud", method="read_json"
+        )
+        with caplog.at_level(level=logging.INFO, logger=DEFAULT_LOGGER_NAME):
+            assert (
+                pd.read_json(
+                    StringIO(
+                        json.dumps({"col0": list(range(BIG_DATA_CLOUD_MIN_NUM_ROWS))})
+                    )
+                ).get_backend()
+                == "Big_Data_Cloud"
+            )
+        log_records = caplog.records
+        assert len(log_records) == 2
+
+        assert log_records[0].name == DEFAULT_LOGGER_NAME
+        assert log_records[0].levelno == logging.INFO
+        assert log_records[0].message.startswith(
+            "After None function read_json, considered moving to backend Small_Data_Local with"
+        )
+
+        assert log_records[1].name == DEFAULT_LOGGER_NAME
+        assert log_records[1].levelno == logging.INFO
+        assert log_records[1].message.startswith(
+            "Chose not to switch backends after operation read_json"
+        )
+
     def test_agg(self):
         with backend_test_context(
             test_backend="Big_Data_Cloud",