Repository: narwhals-dev/narwhals (python)

Issue:
Different error-ing behaviour with `native_namespace=pd` compared to the orginal pandas?
I am not sure if I do something wrong or if this is on purpose, but it seems that narwhals has a different error-ing behaviour with `native_namespace=pd` as pandas itself (at least in my little use case):

I also wonder why narwhals doesn't raise their own errors, so that libraries using narwhals don't need to check for all the possible error types and messages in their tests?

Reproducible:

```python
import narwhals.stable.v1 as nw
import pandas as pd
```

```python
# using pandas:
first = pd.Series([1, 2, 3], name="first")
second = pd.Series([1, 2, 3, 4], name="second")

df = pd.DataFrame(first)
df["second"] = second 
```

This does not error: it aligns itself
`print(df)`

>    first  second
> 0      1       1
> 1      2       2
> 2      3       3

```python
# using narwhals:
first = nw.new_series(name="first", values=[1,2,3], native_namespace=pd)
second = nw.new_series(name="second", values=[1,2,3,4], native_namespace=pd)

dict = nw.from_dict({"first": first}, backend=pd)
dict = dict.with_columns(**{"second": second})
```

This raises a pandas error:

>  ValueError: Length mismatch: Expected axis has 4 elements, new values have 3 elements

Extract from Traceback:

```
fairlearn/reductions/_moments/moment.py:63: in load_data
    self.tags = self.tags.with_columns(**{_GROUP_ID: sensitive_features})
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/narwhals/dataframe.py:1286: in with_columns
    return super().with_columns(*exprs, **named_exprs)
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/narwhals/dataframe.py:146: in with_columns
    self._compliant_frame.with_columns(*compliant_exprs),
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/narwhals/_pandas_like/dataframe.py:488: in with_columns
    to_concat.extend(
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/narwhals/_pandas_like/dataframe.py:489: in <genexpr>
    extract_dataframe_comparand(index, new_column_name_to_new_column_map[s])
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/narwhals/_pandas_like/utils.py:141: in extract_dataframe_comparand
    return set_index(
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/narwhals/_pandas_like/utils.py:311: in set_index
    return obj.set_axis(index, axis=0, **kwargs)  # type: ignore[attr-defined]
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/pandas/core/series.py:5133: in set_axis
    return super().set_axis(labels, axis=axis, copy=copy)
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/pandas/core/generic.py:792: in set_axis
    return self._set_axis_nocheck(labels, axis, inplace=False, copy=copy)
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/pandas/core/generic.py:804: in _set_axis_nocheck
    setattr(obj, obj._get_axis_name(axis), labels)
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/pandas/core/generic.py:6313: in __setattr__
    return object.__setattr__(self, name, value)
properties.pyx:69: in pandas._libs.properties.AxisProperty.__set__
    ???
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/pandas/core/generic.py:814: in _set_axis
    self._mgr.set_axis(axis, labels)
../../../.pyenv/versions/3.12.2/envs/fairlearn_dev/lib/python3.12/site-packages/pandas/core/internals/managers.py:238: in set_axis
    self._validate_set_axis(axis, new_labels)

ValueError: Length mismatch: Expected axis has 4 elements, new values have 3 elements
```

Pull request description:
feat: Raise uniform error when passing Series of different lengths to `with_columns`
closes #2197

<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues

- Related issue #\<issue number\>
- Closes #\<issue number\>

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below


Patch:
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 2d4f8544..7edbe473 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -13,6 +13,7 @@ from typing import overload
 import pyarrow as pa
 import pyarrow.compute as pc
 
+from narwhals.exceptions import ShapeError
 from narwhals.utils import _SeriesNamespace
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import isinstance_or_issubclass
@@ -278,6 +279,9 @@ def extract_dataframe_comparand(
 ) -> ArrowChunkedArray:
     """Extract native Series, broadcasting to `length` if necessary."""
     if not other._broadcast:
+        if (len_other := len(other)) != length:
+            msg = f"Expected object of length {length}, got: {len_other}."
+            raise ShapeError(msg)
         return other.native
 
     import numpy as np  # ignore-banned-import
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 0fb7b352..30bf6c18 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -7,6 +7,7 @@ from contextlib import suppress
 from typing import TYPE_CHECKING
 from typing import Any
 from typing import Iterable
+from typing import Sized
 from typing import TypeVar
 from typing import cast
 
@@ -14,12 +15,13 @@ import pandas as pd
 
 from narwhals.exceptions import ColumnNotFoundError
 from narwhals.exceptions import DuplicateError
+from narwhals.exceptions import ShapeError
 from narwhals.utils import Implementation
 from narwhals.utils import Version
 from narwhals.utils import import_dtypes_module
 from narwhals.utils import isinstance_or_issubclass
 
-T = TypeVar("T")
+T = TypeVar("T", bound=Sized)
 
 if TYPE_CHECKING:
     from pandas._typing import Dtype as PandasDtype
@@ -137,6 +139,9 @@ def extract_dataframe_comparand(
     if other._broadcast:
         s = other._native_series
         return s.__class__(s.iloc[0], index=index, dtype=s.dtype, name=s.name)
+    if (len_other := len(other)) != (len_idx := len(index)):
+        msg = f"Expected object of length {len_idx}, got: {len_other}."
+        raise ShapeError(msg)
     if other._native_series.index is not index:
         return set_index(
             other._native_series,


Test patch:
diff --git a/tests/frame/with_columns_test.py b/tests/frame/with_columns_test.py
index 5756bb9b..c793a3e0 100644
--- a/tests/frame/with_columns_test.py
+++ b/tests/frame/with_columns_test.py
@@ -5,6 +5,7 @@ import pandas as pd
 import pytest
 
 import narwhals.stable.v1 as nw
+from narwhals.exceptions import ShapeError
 from tests.utils import PYARROW_VERSION
 from tests.utils import Constructor
 from tests.utils import ConstructorEager
@@ -68,3 +69,12 @@ def test_with_columns_dtypes_single_row(
     df = nw.from_native(constructor(data)).with_columns(nw.col("a").cast(nw.Categorical))
     result = df.with_columns(nw.col("a"))
     assert result.collect_schema() == {"a": nw.Categorical}
+
+
+def test_with_columns_series_shape_mismatch(constructor_eager: ConstructorEager) -> None:
+    df1 = nw.from_native(constructor_eager({"first": [1, 2, 3]}), eager_only=True)
+    second = nw.from_native(constructor_eager({"second": [1, 2, 3, 4]}), eager_only=True)[
+        "second"
+    ]
+    with pytest.raises(ShapeError):
+        df1.with_columns(second=second)