Repository: pandas-dev/pandas
Pull request: BUG: repr aligning left for string dtype columns
- [ ] closes #54797 (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

cc @jorisvandenbossche this is a bit hacky, but changing format_array breaks index-related stuff. Don't feel comfortable to do this now, can be a follow up (we are casting to object anyway, so this does not impact performance)

Problem statement:
BUG: wrong alignment of column names in the DataFrame repr when using pyarrow-backed string dtype
When enabling the future string options, I see a wrong alignment of the column names:

```
In [10]: pd.options.future.infer_string = True

In [11]: df = pd.DataFrame({"long_column_name": [1, 2, 3], "col2": [4, 5, 6]})

In [12]: df
Out[12]: 
   long_column_name  col2            
0                 1                 4
1                 2                 5
2                 3                 6
```

The names seem left aligned instead of right? And every column uses the width needed for the widest column.

This happens when the Index object backing the columns uses the string dtype. 
It seem this also happens for the the other String dtype instantiations (eg after doing `df.columns = df.columns.astype("string[python]")`, i.e. not Arrow specific), but in general it's harder to get a DataFrame with such column names with using `pd.options.mode.string_storage`, since you still need to manually specify the dtype.

cc @phofl

Hints from issue discussion:

Commit message: BUG: repr aligning left for string dtype columns

Patch to review:
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 19a8500928ab7..8f2667d69a322 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -717,6 +717,7 @@ Conversion
 Strings
 ^^^^^^^
 - Bug in :meth:`Series.str` that did not raise a  ``TypeError`` when iterated (:issue:`54173`)
+- Bug in ``repr`` for :class:`DataFrame`` with string-dtype columns (:issue:`54797`)
 
 Interval
 ^^^^^^^^
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index a49db84450bb3..3fa1d8b3ec48a 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -1395,6 +1395,8 @@ def _format_with_header(self, header: list[str_t], na_rep: str_t) -> list[str_t]
         from pandas.io.formats.format import format_array
 
         values = self._values
+        if is_string_dtype(values.dtype):
+            values = np.asarray(values)
 
         if is_object_dtype(values.dtype):
             values = cast(np.ndarray, values)
diff --git a/pandas/tests/frame/test_repr_info.py b/pandas/tests/frame/test_repr_info.py
index 49375658abfee..64d516e484991 100644
--- a/pandas/tests/frame/test_repr_info.py
+++ b/pandas/tests/frame/test_repr_info.py
@@ -455,3 +455,14 @@ def test_masked_ea_with_formatter(self):
 0  0.12  1.00
 1  1.12  2.00"""
         assert result == expected
+
+    def test_repr_ea_columns(self, any_string_dtype):
+        # GH#54797
+        pytest.importorskip("pyarrow")
+        df = DataFrame({"long_column_name": [1, 2, 3], "col2": [4, 5, 6]})
+        df.columns = df.columns.astype(any_string_dtype)
+        expected = """   long_column_name  col2
+0                 1     4
+1                 2     5
+2                 3     6"""
+        assert repr(df) == expected


Code review comments:
diff_hunk: @@ -1395,6 +1395,8 @@ def _format_with_header(self, header: list[str_t], na_rep: str_t) -> list[str_t]
         from pandas.io.formats.format import format_array
 
         values = self._values
+        if is_string_dtype(values.dtype):
+            values = np.asarray(values)
path: pandas/core/indexes/base.py
text: @user1:
```suggestion
        if is_string_dtype(values.dtype):
            # ensure we have an object dtype numpy array
            values = np.asarray(values)
```

diff_hunk: @@ -1395,6 +1395,8 @@ def _format_with_header(self, header: list[str_t], na_rep: str_t) -> list[str_t]
         from pandas.io.formats.format import format_array
 
         values = self._values
+        if is_string_dtype(values.dtype):
+            values = np.asarray(values)
 
         if is_object_dtype(values.dtype):
             values = cast(np.ndarray, values)
path: pandas/core/indexes/base.py
text: @user1:
Or

```suggestion

        if is_object_dtype(values.dtype) or is_string_dtype(values.dtype):
            values = np.asarray(values)
```

could also be a way to be explicit that string dtypes take this route