Repository: narwhals-dev/narwhals

Issue:
Let non-string names "pass-through" in DataFrame.select
I think
```python
df = pd.DataFrame({0: [1, 2, 3], 1: [4,5,6]})
print(nw.from_native(df, eager_only=True).select(0).pipe(nw.to_native))
```
should be supported

(plus, something like this is used in sklearn)

Discussion:
MarcoGorelli: thanks for your review, all good points!

in theory we'd decided to support this, but it looks like there were a few parts missing (which I noticed testing out scikit-learn)

Patch:
diff --git a/docs/other/column_names.md b/docs/other/column_names.md
new file mode 100644
index 00000000..97767053
--- /dev/null
+++ b/docs/other/column_names.md
@@ -0,0 +1,24 @@
+# Column names
+
+Polars and PyArrow only allow for string column names. What about pandas?
+
+```python
+>>> import pandas as pd
+>>> pd.concat([pd.Series([1, 2], name=0), pd.Series([1, 3], name=0)], axis=1)
+   0  0
+0  1  1
+1  2  3
+```
+
+Oh...not only does it let us create a dataframe with a column named `0` - it lets us
+create one with _two_ such columns!
+
+What does Narwhals do about this?
+
+- In general, non-string column names are supported. In some places where this might
+  create ambiguity (such as `DataFrame.__getitem__`) we may be strict and only
+  allow passing in column names if they're strings.
+- If you have a use-case that's
+  failing for non-string column names, please report it to [https://github.com/narwhals-dev/narwhals/issues](https://github.com/narwhals-dev/narwhals/issues)
+  and we'll see if we can support it.
+- Duplicate column names are 🚫 banned 🚫.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index f0397ca3..87b224bf 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,8 +1,11 @@
 # Roadmap
 
-Priorities, as of July 2024, are:
+Priorities, as of August 2024, are:
 
-- Works towards supporting projects which have shown interest in Narwhals: scikit-learn, shiny, tubular
-- Implement when/then/otherwise so that Narwhals is API-complete enough to complete all the TPC-H queries
-- Add support for extra backends such as Dask
-- Add extra docs and tutorials to make the project more accessible and easy to get started with
+- Works towards supporting projects which have shown interest in Narwhals.
+- Implement when/then/otherwise so that Narwhals is API-complete enough to complete all the TPC-H queries.
+- Make Dask support complete-enough, at least to the point that it can execute TPC-H queries.
+- Improve support for cuDF, which we can't currently test in CI (unless NVIDIA helps us out :wink:) but
+  which we can and do test manually in Kaggle notebooks.
+- Add extra docs and tutorials to make the project more accessible and easy to get started with.
+- Look into extra backends, such as DuckDB and Ibis.
diff --git a/mkdocs.yml b/mkdocs.yml
index 5c8549fc..f222610e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -11,9 +11,10 @@ nav:
     - basics/dataframe.md
     - basics/column.md
     - basics/complete_example.md
-  - Other concepts:
+  - Pandas-like concepts:
     - other/pandas_index.md
     - other/user_warning.md
+    - other/column_names.md
   - levels.md
   - overhead.md
   - backcompat.md
diff --git a/narwhals/_expression_parsing.py b/narwhals/_expression_parsing.py
index 4c642239..161c3aca 100644
--- a/narwhals/_expression_parsing.py
+++ b/narwhals/_expression_parsing.py
@@ -3,6 +3,7 @@
 # and pandas or PyArrow.
 from __future__ import annotations
 
+from collections.abc import Hashable
 from copy import copy
 from typing import TYPE_CHECKING
 from typing import Any
@@ -168,11 +169,12 @@ def parse_into_expr(
         return into_expr  # type: ignore[return-value]
     if hasattr(into_expr, "__narwhals_series__"):
         return namespace._create_expr_from_series(into_expr)  # type: ignore[arg-type]
-    if isinstance(into_expr, str):
-        return namespace.col(into_expr)
+    if isinstance(into_expr, Hashable):
+        # pandas allows non-string column names, so we pass them through here.
+        return namespace.col(into_expr)  # type: ignore[arg-type]
     if is_numpy_array(into_expr):
         series = namespace._create_compliant_series(into_expr)
-        return namespace._create_expr_from_series(series)  # type: ignore[arg-type]
+        return namespace._create_expr_from_series(series)
     msg = f"Expected IntoExpr, got {type(into_expr)}"  # pragma: no cover
     raise AssertionError(msg)
 


Test patch:
diff --git a/tests/frame/select_test.py b/tests/frame/select_test.py
index 0e25cc87..27a45ff0 100644
--- a/tests/frame/select_test.py
+++ b/tests/frame/select_test.py
@@ -1,5 +1,7 @@
 from typing import Any
 
+import pandas as pd
+
 import narwhals.stable.v1 as nw
 from tests.utils import compare_dicts
 
@@ -15,3 +17,9 @@ def test_select(constructor: Any) -> None:
 def test_empty_select(constructor: Any) -> None:
     result = nw.from_native(constructor({"a": [1, 2, 3]})).lazy().select()
     assert result.collect().shape == (0, 0)
+
+
+def test_non_string_select() -> None:
+    df = nw.from_native(pd.DataFrame({0: [1, 2], "b": [3, 4]}))
+    result = nw.to_native(df.select(0))  # type: ignore[arg-type]
+    pd.testing.assert_frame_equal(result, pd.Series([1, 2], name=0).to_frame())