Repository: sdv-dev/RDT (2025-03-25 ~ 2025-04-10)

problem_statement: `BaseTransformer.__repr__` errors if transformer has positional arguments
### Environment Details

Please indicate the following details about the environment in which you found the bug:

* RDT version:
* Python version:
* Operating System:

### Error Description
The `__repr__` method on the base transformer tries to pretty print each transformer with the arguments it was called with. However, this method expects all arguments to be keyword arguments, so transformers that have positional arguments will cause an error.

We should update the `__repr__` method so it works as expected with required and keyword arguments.

### Steps to reproduce
```python
from rdt.transformers.categorical import DPResponseRandomizer

transformer = DPResponseRandomizer(1)
print(transformer)
```

date: 2025-03-25
patch: diff --git a/rdt/transformers/base.py b/rdt/transformers/base.py
index 808e32a..1863462 100644
--- a/rdt/transformers/base.py
+++ b/rdt/transformers/base.py
@@ -327,19 +327,19 @@ class BaseTransformer:
         custom_args = []
         args = inspect.getfullargspec(self.__init__)
         keys = args.args[1:]
-        defaults = args.defaults or []
-        defaults = dict(zip(keys, defaults))
         instanced = {
             key: getattr(self, key)
             for key in keys
             if key != 'model_missing_values' and hasattr(self, key)  # Remove after deprecation
         }
 
+        defaults = args.defaults or []
+        defaults = dict(zip(keys, defaults))
         if defaults == instanced:
             return f'{class_name}()'
 
         for arg, value in instanced.items():
-            if defaults[arg] != value:
+            if arg not in defaults or defaults[arg] != value:
                 custom_args.append(f'{arg}={repr(value)}')
 
         args_string = ', '.join(custom_args)

test_patch: diff --git a/tests/integration/test_hyper_transformer.py b/tests/integration/test_hyper_transformer.py
index 2eb4381..ff6b82a 100644
--- a/tests/integration/test_hyper_transformer.py
+++ b/tests/integration/test_hyper_transformer.py
@@ -958,6 +958,39 @@ class TestHyperTransformer:
         # Run and Assert
         ht.set_config(config)
 
+    def test_set_config_with_duplicated_transformers(self):
+        """Test ``set_config`` errors if the same transformer is used more than once."""
+        # Setup
+        transformer = UniformEncoder()
+        multi_column_transformer = DummyMultiColumnTransformerNumerical()
+        config = {
+            'transformers': {
+                'categorical_col1': transformer,
+                'categorical_col2': transformer,
+                ('numerical_col1', 'numerical_col2'): multi_column_transformer,
+                ('numerical_col3', 'numerical_col4'): multi_column_transformer,
+            },
+            'sdtypes': {
+                'categorical_col1': 'categorical',
+                'categorical_col2': 'categorical',
+                'numerical_col1': 'numerical',
+                'numerical_col2': 'numerical',
+                'numerical_col3': 'numerical',
+                'numerical_col4': 'numerical',
+            },
+        }
+        ht = HyperTransformer()
+
+        # Run and Assert
+        expected_msg = re.escape(
+            "The same transformer instances are being assigned to columns ('categorical_col1', "
+            "'categorical_col2'), columns (('numerical_col1', 'numerical_col2'), "
+            "('numerical_col3', 'numerical_col4')). Please create different transformer objects "
+            'for each assignment.'
+        )
+        with pytest.raises(InvalidConfigError, match=expected_msg):
+            ht.set_config(config)
+
     def test_chained_transformers(self):
         """Test when a transformer is chained to another transformer.
 
@@ -1780,6 +1813,61 @@ class TestHyperTransformer:
         assert repr(new_config) == repr(expected_config)
         assert ht._multi_column_fields == expected_multi_columns
 
+    def test_update_transformers_reused_transformer_instance(self):
+        """Test ``update_transformers`` errors if duplicate transformer instance is used."""
+        # Setup
+        transformer = UniformEncoder()
+        dict_config = {
+            'sdtypes': {
+                'A': 'categorical',
+                'B': 'categorical',
+                'C': 'categorical',
+            },
+            'transformers': {'A': UniformEncoder(), 'B': transformer, 'C': None},
+        }
+
+        config = Config(dict_config)
+        ht = HyperTransformer()
+        ht.set_config(config)
+
+        # Run and Assert
+        expected_msg = re.escape(
+            "The transformer for column ('C') is already assigned to column ('B'). "
+            'Please create different transformer objects for each assignment.'
+        )
+        with pytest.raises(InvalidConfigError, match=expected_msg):
+            ht.update_transformers({'C': transformer})
+
+    def test_update_transformers_reused_multi_column_transformer_instance(self):
+        """Test ``update_transformers`` errors with duplicated multi-column transformer instance."""
+        # Setup
+        transformer = DummyMultiColumnTransformerNumerical()
+        dict_config = {
+            'sdtypes': {
+                'A': 'categorical',
+                'B': 'categorical',
+                'C': 'categorical',
+                'D': 'categorical',
+            },
+            'transformers': {
+                ('A', 'B'): transformer,
+                'C': None,
+                'D': None,
+            },
+        }
+
+        config = Config(dict_config)
+        ht = HyperTransformer()
+        ht.set_config(config)
+
+        # Run and Assert
+        expected_msg = re.escape(
+            "The transformer for columns ('C', 'D') is already assigned to columns ('A', 'B'). "
+            'Please create different transformer objects for each assignment.'
+        )
+        with pytest.raises(InvalidConfigError, match=expected_msg):
+            ht.update_transformers({('C', 'D'): transformer})
+
     def test_remove_transformer(self):
         """Test ``remove_transformer`` with multi column transformer."""
         # Setup
diff --git a/tests/unit/test__utils.py b/tests/unit/test__utils.py
new file mode 100644
index 0000000..b698cf2
--- /dev/null
+++ b/tests/unit/test__utils.py
@@ -0,0 +1,83 @@
+import re
+
+import pytest
+
+from rdt._utils import _validate_unique_transformer_instances
+from rdt.errors import (
+    InvalidConfigError,
+)
+from rdt.transformers import (
+    BaseMultiColumnTransformer,
+    BaseTransformer,
+)
+
+
+@pytest.fixture()
+def column_name_to_transformer():
+    return {
+        'colA': BaseTransformer(),
+        'colB': BaseTransformer(),
+        'colC': BaseTransformer(),
+        ('colD', 'colE'): BaseMultiColumnTransformer(),
+        ('colF', 'colG'): BaseMultiColumnTransformer(),
+        'colH': None,
+        'colI': None,
+    }
+
+
+def test__validate_unique_transformer_instances_no_duplicates(column_name_to_transformer):
+    """Test the function does not error when no duplicate transformers are present."""
+    # Run and Assert
+    _validate_unique_transformer_instances(column_name_to_transformer)
+
+
+def test__validate_unique_transformer_instances_one_duplicate(column_name_to_transformer):
+    """Test the function errors when one transformer instance is reused."""
+    # Setup
+    column_name_to_transformer = column_name_to_transformer.copy()
+    column_name_to_transformer['duped_column_1'] = column_name_to_transformer['colA']
+    column_name_to_transformer['duped_column_2'] = column_name_to_transformer['colA']
+
+    # Run and Assert
+    expected_msg = re.escape(
+        "The same transformer instance is being assigned to columns ('colA', 'duped_column_1', "
+        "'duped_column_2'). Please create different transformer objects for each assignment."
+    )
+    with pytest.raises(InvalidConfigError, match=expected_msg):
+        _validate_unique_transformer_instances(column_name_to_transformer)
+
+
+def test__validate_unique_transformer_instances_multi_column(column_name_to_transformer):
+    """Test the function with multi-column transformers."""
+    # Setup
+    column_name_to_transformer = column_name_to_transformer.copy()
+    duplicate_transformer = column_name_to_transformer[('colD', 'colE')]
+    column_name_to_transformer[('duped_column_1', 'duped_column_2')] = duplicate_transformer
+
+    # Run and Assert
+    expected_msg = re.escape(
+        "The same transformer instance is being assigned to columns (('colD', 'colE'), "
+        "('duped_column_1', 'duped_column_2')). Please create different transformer "
+        'objects for each assignment.'
+    )
+    with pytest.raises(InvalidConfigError, match=expected_msg):
+        _validate_unique_transformer_instances(column_name_to_transformer)
+
+
+def test__validate_unique_transformer_instances_multiple_duplicates(column_name_to_transformer):
+    """Test the function errors when many transformer instances are reused."""
+    # Setup
+    column_name_to_transformer = column_name_to_transformer.copy()
+    column_name_to_transformer['duped_column_1'] = column_name_to_transformer['colA']
+    column_name_to_transformer['duped_column_2'] = column_name_to_transformer['colA']
+    column_name_to_transformer['duped_column_3'] = column_name_to_transformer['colB']
+    column_name_to_transformer['duped_column_4'] = column_name_to_transformer['colB']
+
+    # Run and Assert
+    expected_msg = re.escape(
+        "The same transformer instances are being assigned to columns ('colA', 'duped_column_1', "
+        "'duped_column_2'), columns ('colB', 'duped_column_3', 'duped_column_4'). Please create "
+        'different transformer objects for each assignment.'
+    )
+    with pytest.raises(InvalidConfigError, match=expected_msg):
+        _validate_unique_transformer_instances(column_name_to_transformer)
diff --git a/tests/unit/test_hyper_transformer.py b/tests/unit/test_hyper_transformer.py
index 6b5354c..3be843e 100644
--- a/tests/unit/test_hyper_transformer.py
+++ b/tests/unit/test_hyper_transformer.py
@@ -2611,6 +2611,23 @@ class TestHyperTransformer(TestCase):
         assert str(ht.field_transformers) == str(expected_field_transformers)
         assert ht._multi_column_fields == expected_multi_column_fieds
 
+    def test__validate_updated_transformer_unique(self):
+        """Test method errors if an updated transformer already exists in the config."""
+        # Setup
+        transformer = FrequencyEncoder()
+        instance = HyperTransformer()
+        instance.field_transformers = {'existing_column': transformer, 'my_column': object()}
+
+        # Run and Assert
+        instance._validate_updated_transformer_unique('existing_column', transformer)
+        expected_message = re.escape(
+            "The transformer for column ('my_column') is already assigned to "
+            "column ('existing_column'). Please create different transformer objects for "
+            'each assignment.'
+        )
+        with pytest.raises(InvalidConfigError, match=expected_message):
+            instance._validate_updated_transformer_unique('my_column', transformer)
+
     def test_update_transformers_with_multi_column(self):
         """Test ``update_transformers`` with a multi-column transformer."""
         # Setup
@@ -3339,6 +3356,25 @@ class TestHyperTransformer(TestCase):
         with pytest.raises(InvalidConfigError, match=error_msg):
             instance._validate_transformers(column_name_to_transformer)
 
+    def test__validate_transformers_reused_instances(self):
+        """Test ``_validate_transformers`` with reused transformer isntances."""
+        # Setup
+        instance = HyperTransformer()
+        duplicated_transformer = FrequencyEncoder()
+        column_name_to_transformer = {
+            'col1': duplicated_transformer,
+            'col2': duplicated_transformer,
+            'col3': None,
+        }
+
+        # Run / Assert
+        error_msg = re.escape(
+            "The same transformer instance is being assigned to columns ('col1', 'col2'). "
+            'Please create different transformer objects for each assignment.'
+        )
+        with pytest.raises(InvalidConfigError, match=error_msg):
+            instance._validate_transformers(column_name_to_transformer)
+
     def test_remove_transformers(self):
         """Test the ``remove_transformers`` method.
 
diff --git a/tests/unit/transformers/test_base.py b/tests/unit/transformers/test_base.py
index b2edc93..248f3bc 100644
--- a/tests/unit/transformers/test_base.py
+++ b/tests/unit/transformers/test_base.py
@@ -364,18 +364,19 @@ class TestBaseTransformer:
 
         # Setup
         class Dummy(BaseTransformer):
-            def __init__(self, param1=None, param2=None, param3=None):
+            def __init__(self, param0, param1=None, param2=None, param3=None):
+                self.param0 = param0
                 self.param1 = param1
                 self.param2 = param2
                 self.param3 = param3
 
-        transformer = Dummy(param2='value', param3=True)
+        transformer = Dummy(param0='required', param2='value', param3=True)
 
         # Run
         text = repr(transformer)
 
         # Assert
-        assert text == "Dummy(param2='value', param3=True)"
+        assert text == "Dummy(param0='required', param2='value', param3=True)"
 
     def test__str__(self):
         """Test the ``__str__`` method.
diff --git a/tests/unit/transformers/test_utils.py b/tests/unit/transformers/test_utils.py
index 34fb03a..5b10d95 100644
--- a/tests/unit/transformers/test_utils.py
+++ b/tests/unit/transformers/test_utils.py
@@ -2,7 +2,7 @@ import sre_parse
 import warnings
 from decimal import Decimal
 from sre_constants import MAXREPEAT
-from unittest.mock import Mock, patch
+from unittest.mock import Mock
 
 import numpy as np
 import pandas as pd
@@ -222,41 +222,80 @@ def test_try_convert_to_dtype():
     pd.testing.assert_series_equal(output_convertibe, expected_data_convertibe)
 
 
-@patch('rdt.transformers.utils.LOGGER')
-def test_learn_rounding_digits_more_than_15_decimals(logger_mock):
-    """Test the learn_rounding_digits method with more than 15 decimals.
+@pytest.mark.parametrize(
+    'test_data, expected_digits',
+    [
+        # Basic decimal places test
+        (pd.Series([10, 0.0, 0.1, 0.12, 0.123, np.nan]), 3),
+        # Large numbers with decimals
+        (pd.Series([1234567890123456.7, 12345678901234567.89, 123456789012345678.901]), None),
+        # Consistent single decimal place
+        (
+            pd.Series([
+                1.1,
+                11.1,
+                111.1,
+                1111.1,
+                11111.1,
+                111111.1,
+                1111111.1,
+                11111111.1,
+                111111111.1,
+                1111111111.1,
+                11111111111.1,
+                111111111111.1,
+                1111111111111.1,
+                11111111111111.1,
+            ]),
+            1,
+        ),
+        # Various precision tests
+        (pd.Series([123456789012345.6789]), None),
+        (pd.Series([12345678901.234]), 3),
+        (pd.Series([12345678901.2345]), 4),
+        (pd.Series([0.1234567890123456]), None),
+        (pd.Series([0.123456789012345]), 15),
+        # Integer tests
+        (pd.Series([123456789012345]), 0),
+        (pd.Series([12345678901234567890]), 0),
+        # Mixed number and edge cases
+        (pd.Series([12345678901234567890, 1.123]), None),
+        (pd.Series([0.000000000000000000001]), None),
+        (pd.Series([-12345678901234, -1.123]), None),
+        (pd.Series([-0.123456789012345]), 15),
+    ],
+)
+def test_learn_rounding_digits(test_data, expected_digits):
+    """Test learn_rounding_digits for various test cases."""
+    # Run
+    result = learn_rounding_digits(test_data)
 
-    If the data has more than 15 decimals, return None and raise warning.
-    """
+    # Assert
+    assert result == expected_digits
+
+
+def test_learn_rounding_digits_object_dtype():
+    """Test learn_rounding_digits for object dtype."""
     # Setup
-    data = pd.Series(np.random.random(size=10).round(20), name='col')
+    data = pd.Series(['1.1', '1.11', np.nan], dtype='object')
 
     # Run
-    output = learn_rounding_digits(data)
+    result = learn_rounding_digits(data)
 
     # Assert
-    logger_msg = "No rounding scheme detected for column '%s'. Data will not be rounded."
-    logger_mock.info.assert_called_once_with(logger_msg, 'col')
-    assert output is None
-
-
-def test_learn_rounding_digits_less_than_15_decimals():
-    """Test the learn_rounding_digits method with less than 15 decimals.
+    assert result == 2
 
-    If the data has less than 15 decimals, the maximum number of decimals
-    should be returned.
 
-    Input:
-    - An array that contains floats with a maximum of 3 decimals and a
-        NaN.
-    Output:
-    - 3
-    """
-    data = pd.Series(np.array([10, 0.0, 0.1, 0.12, 0.123, np.nan]))
+def test_learn_rounding_digits_code_coverage():
+    """Test learn_rounding_digits for code coverage."""
+    # Setup
+    data = pd.Series([np.inf, -np.inf, np.nan])
 
-    output = learn_rounding_digits(data)
+    # Run
+    result = learn_rounding_digits(data)
 
-    assert output == 3
+    # Assert
+    assert result is None
 
 
 def test_learn_rounding_digits_pyarrow():
@@ -289,54 +328,6 @@ def test_learn_rounding_digits_pyarrow_float():
     assert output == 2
 
 
-def test_learn_rounding_digits_negative_decimals_float():
-    """Test the learn_rounding_digits method with floats multiples of powers of 10.
-
-    If the data has all multiples of 10 the output should be 0.
-
-    Input:
-    - An array that contains floats that are multiples of powers of 10, 100 and 1000 and a NaN.
-    """
-    data = pd.Series(np.array([1230.0, 12300.0, 123000.0, np.nan]))
-
-    output = learn_rounding_digits(data)
-
-    assert output == 0
-
-
-def test_learn_rounding_digits_negative_decimals_integer():
-    """Test the learn_rounding_digits method with integers multiples of powers of 10.
-
-    If the data has all multiples of 10 the output should be 0.
-
-    Input:
-    - An array that contains integers that are multiples of powers of 10, 100 and 1000
-        and a NaN.
-    """
-    data = pd.Series(np.array([1230, 12300, 123000, np.nan]))
-
-    output = learn_rounding_digits(data)
-
-    assert output == 0
-
-
-def test_learn_rounding_digits_all_missing_value_replacements():
-    """Test the learn_rounding_digits method with data that is all NaNs.
-
-    If the data is all NaNs, expect that the output is None.
-
-    Input:
-    - An array of NaN.
-    Output:
-    - None
-    """
-    data = pd.Series(np.array([np.nan, np.nan, np.nan, np.nan]))
-
-    output = learn_rounding_digits(data)
-
-    assert output is None
-
-
 def test_learn_rounding_digits_nullable_numerical_pandas_dtypes():
     """Test that ``learn_rounding_digits`` supports the nullable numerical pandas dtypes."""
     # Setup


----

problem_statement: `learn_rounding_digits` should work for numerical data of `object` dtype
### Problem Description
The `learn_rounding_digits` method may raise the following error when running on numerical data of dtype `object`: ```TypeError: loop of ufunc does not support argument 0 of type float which has no callable rint method```.

This happens because pandas.Series doesn't support rounding when `dtype=object` for versions 2.1 or lower of pandas.

### Expected behavior
The `learn_rounding_digits` method should accept as input pandas.Series of numerical data with dtype `object`.

date: 2025-03-31
patch: diff --git a/rdt/transformers/utils.py b/rdt/transformers/utils.py
index 187bd90..868a3e6 100644
--- a/rdt/transformers/utils.py
+++ b/rdt/transformers/utils.py
@@ -15,7 +15,7 @@ import sre_parse  # isort:skip
 
 LOGGER = logging.getLogger(__name__)
 
-MAX_DECIMALS = sys.float_info.dig - 1
+MAX_DECIMALS = sys.float_info.dig
 DEPRECATED_SDTYPES_MAPPING = {'text': 'id'}
 
 
@@ -270,17 +270,19 @@ def learn_rounding_digits(data):
         data = data.to_numpy()
     roundable_data = data[~(np.isinf(data.astype(float)) | pd.isna(data))]
 
-    # Doesn't contain numbers
+    # Empty dataset
     if len(roundable_data) == 0:
         return None
 
-    # Doesn't contain decimal digits
-    if (roundable_data == roundable_data.astype(int)).all():
-        return 0
+    if roundable_data.dtype == 'object':
+        roundable_data = roundable_data.astype(float)
 
     # Try to round to fewer digits
-    if (roundable_data == roundable_data.round(MAX_DECIMALS)).all():
-        for decimal in range(MAX_DECIMALS + 1):
+    highest_int = int(np.max(np.abs(roundable_data)))
+    most_digits = len(str(highest_int)) if highest_int != 0 else 0
+    max_decimals = max(0, MAX_DECIMALS - most_digits)
+    if (roundable_data == roundable_data.round(max_decimals)).all():
+        for decimal in range(max_decimals + 1):
             if (roundable_data == roundable_data.round(decimal)).all():
                 return decimal
 


----

problem_statement: HyperTransformer should ensure that each column is assigned to a different transformer instance
### Problem Description
The HyperTransformer is designed to work by assigning a different transformer _instance_ to each of the columns. Each transformer instance is responsible for (a) learning/saving information about the column during `fit`, and (b) using the saved information during the `transform` and `reverse_transform` phases.

Different information needs to be learned/saved for each column. Therefore, it is not feasible to assign different columns to the same transformer instance.

Currently, the HyperTransformer does not check for this. It allows me to assign the same instance to multiple columns, which can cause misleading error messages and/or data issues later down-the-line, when it is harder to debug.

### Replication
The following code assigns the same instance of `UniformEncoder` to both columns `'A'` and `'B'` in the data.

```python
import pandas as pd
from rdt import HyperTransformer
from rdt.transformers.categorical import UniformEncoder

test_data = pd.DataFrame(data={
    'A': ['yes', 'yes', 'no', 'no', 'no'],
    'B': [True, False, True, True, False] })

# create an instance of the transformer
transformer = UniformEncoder()

# assign the same transformer instance to 'A' and 'B'
ht = HyperTransformer()
ht.set_config({
    'sdtypes': {
        'A': 'categorical',
        'B': 'categorical'
    },
    'transformers': {
        'A': transformer,
        'B': transformer
    }
})
ht.fit_transform(test_data)
```

The following warnings are printed out, but there is nothing indicating that this is due to the same transformer instance:
```
/usr/local/lib/python3.11/dist-packages/rdt/transformers/categorical.py:175: UserWarning:

The data in column 'B' contains new categories that did not appear during 'fit' (0.4625401125705246, 0.6773115734705, 0.2947487449252827, +2 more). Assigning them random values. If you want to model new categories, please fit the data again using 'fit'.

/usr/local/lib/python3.11/dist-packages/rdt/transformers/categorical.py:185: FutureWarning:

Setting an item of incompatible dtype is deprecated and will raise an error in a future version of pandas. Value '[ True False False  True False]' has dtype incompatible with float64, please explicitly cast to a compatible dtype first.
```

And observe how the transformed data isn't fully numerical -- column `'A'` is left with categorical values.

<img width="758" alt="Image" src="https://github.com/user-attachments/assets/bde2b82c-ca91-4a97-a9bb-1f13b707d25a" />

This will cause problems down the line, eg if you are trying to then model this data using Copulas.

### Expected behavior
The HyperTransformer should always check to make that a transformer instance is not shared between different columns. This check should be made every time the config is updated by the user. Namely:
- [`set_config`](https://docs.sdv.dev/rdt/usage/hypertransformer/configuration#set_config) and
- [`update_transformers`](https://docs.sdv.dev/rdt/usage/hypertransformer/configuration#update_transformers)

During both of these updates, the HyperTransformer should check to make sure that the same instance of a transformer is not being duplicated anywhere. If it is, then we should throw an error asking the user to create a new instance.

**Case 1**: Many columns are being set using the same function (`update_transformers` or `set_config`) using the same transformer. Do not update any of the columns and throw an error instead with the first duplicate instance found.

```python
transformer = UniformEncoder()
ht.update_transformers({
  'A': transformer,
  'B': transformer
})
```

```
InvalidConfigError: The same transformer is being assigned to columns ('A') and ('B'). Please create 
different transformer objects for each assignment.
```

**Case 2**: If a transformer is being set but we already have the same transformer instance in the config somewhere.
```python
transformer = UniformEncoder()

ht.update_transformers({'A': transformer}) # adds this transformer to the config
ht.update_transformers({'B': transformer}) # same instance is being used on a different column!
```
```
InvalidConfigError: The transformer for column ('B') is already assigned to column ('A'). Please create different 
transformer objects for each assignment.
```

### Additional Context
Of course, some transformers themselves work on multi-columns such as [GPSNoiser](https://docs.sdv.dev/rdt/transformers-glossary/deep-data-understanding/gps-coordinates/gpsnoiser#examples). But even in this case, a multi-column transformer can only be used once on a given pair of columns. The error messages can adjust to include the set of columns.

```python
transformer = GPSNoiser(noise_km=10.0)

ht.set_config({
  'sdtypes': {
    'origin_lat': 'latitude',
    'origin_lon': 'longitude',
    'dest_lat': 'latitude',
    'dest_lon': 'longitude'
  },
  'transformers': {
    ('origin_lat', 'origin_lon'): transformer,
    ('dest_lat', 'dest_lon'): transformer
  }
})
```
```
InvalidConfigError: The same transformer is being assigned to columns ('origin_lat', 'origin_lon') and ('dest_lat', 'dest_lon'). Please create 
different transformer objects for each assignment.
```
date: 2025-04-10
patch: diff --git a/rdt/_utils.py b/rdt/_utils.py
new file mode 100644
index 0000000..370d0c8
--- /dev/null
+++ b/rdt/_utils.py
@@ -0,0 +1,47 @@
+from collections import defaultdict
+
+from rdt.errors import InvalidConfigError
+
+
+def _validate_unique_transformer_instances(column_name_to_transformer):
+    """Validate that the transformer instance for each field is unique.
+
+    Args:
+        column_name_to_transformer (dict):
+            A dictionary mapping column names to their current transformer.
+
+    Raises:
+        - ``InvalidConfigError`` if transformers in ``column_name_to_transformer`` are repeated.
+    """
+    seen_transformers = defaultdict(set)
+    for column_name, transformer in column_name_to_transformer.items():
+        if transformer is not None:
+            seen_transformers[transformer].add(column_name)
+
+    duplicated_transformers = {
+        transformer: columns
+        for transformer, columns in seen_transformers.items()
+        if len(columns) > 1
+    }
+    if duplicated_transformers:
+        duplicated_column_messages = []
+        for duplicated_columns in duplicated_transformers.values():
+            columns = ', '.join(
+                sorted([
+                    str(columns) if not isinstance(columns, str) else f"'{columns}'"
+                    for columns in duplicated_columns
+                ])
+            )
+            duplicated_column_messages.append(f'columns ({columns})')
+
+        if len(duplicated_column_messages) > 1:
+            plurality = 'instances are'
+        else:
+            plurality = 'instance is'
+
+        error_message = (
+            f'The same transformer {plurality} being assigned to '
+            f'{", ".join(duplicated_column_messages)}. Please create different transformer objects '
+            'for each assignment.'
+        )
+        raise InvalidConfigError(error_message)
diff --git a/rdt/hyper_transformer.py b/rdt/hyper_transformer.py
index 1ff2e8d..754c0a5 100644
--- a/rdt/hyper_transformer.py
+++ b/rdt/hyper_transformer.py
@@ -8,6 +8,7 @@ from copy import deepcopy
 
 import pandas as pd
 
+from rdt._utils import _validate_unique_transformer_instances
 from rdt.errors import (
     ConfigNotSetError,
     InvalidConfigError,
@@ -181,6 +182,8 @@ class HyperTransformer:
                 'Please assign an rdt transformer instance to each column name.'
             )
 
+        _validate_unique_transformer_instances(column_name_to_transformer)
+
     @staticmethod
     def _validate_sdtypes(sdtypes):
         """Validate the given sdtypes are valid.
@@ -496,6 +499,33 @@ class HyperTransformer:
         if self._fitted:
             warnings.warn(self._REFIT_MESSAGE)
 
+    def _validate_updated_transformer_unique(self, column_name, transformer):
+        """Validate the transformer being updated is not reused in the config."""
+        if (
+            transformer in self.field_transformers.values()
+            and self.field_transformers.get(column_name) != transformer
+        ):
+            existing_column = [
+                column
+                for column, existing_transformer in self.field_transformers.items()
+                if transformer == existing_transformer
+            ][0]
+            column_name = (
+                f"column ('{column_name}')"
+                if isinstance(column_name, str)
+                else f'columns {str(column_name)}'
+            )
+            existing_column = (
+                f"column ('{existing_column}')"
+                if isinstance(existing_column, str)
+                else f'columns {str(existing_column)}'
+            )
+            raise InvalidConfigError(
+                f'The transformer for {column_name} is already assigned to '
+                f'{existing_column}. Please create different transformer objects for '
+                'each assignment.'
+            )
+
     def update_transformers(self, column_name_to_transformer):
         """Update any of the transformers assigned to each of the column names.
 
@@ -514,6 +544,9 @@ class HyperTransformer:
         self._validate_transformers(column_name_to_transformer)
 
         for column_name, transformer in column_name_to_transformer.items():
+            if transformer is not None:
+                self._validate_updated_transformer_unique(column_name, transformer)
+
             columns = column_name if isinstance(column_name, tuple) else (column_name,)
             for column in columns:
                 if transformer is not None: