Repository: narwhals-dev/narwhals
Pull request #308 (2024-06-17 19:34:32)

[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.5.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.5.7)
<!--pre-commit.ci end-->


Patch:
diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml
index e2e2ed1e..bc9003ce 100644
--- a/.github/workflows/publish_to_pypi.yml
+++ b/.github/workflows/publish_to_pypi.yml
@@ -30,7 +30,7 @@ jobs:
   publish-to-pypi:
     name: >-
       Publish Python 🐍 distribution 📦 to PyPI
-    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
+    if: startsWith(github.ref, 'refs/tags/v')  # only publish to PyPI on tag pushes
     needs:
     - build
     runs-on: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 757cff1d..4914e7e1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: 'v0.5.6'
+  rev: 'v0.5.7'
   hooks:
     # Run the formatter.
     - id: ruff-format
@@ -31,8 +31,8 @@ repos:
       additional_dependencies: [polars]
     - id: imports-are-banned
       name: import are banned (use `get_pandas` instead of `import pandas`)
-      entry: (?<!>>> )import (pandas|polars|modin|cudf|pyarrow|dask)
-      language: pygrep
+      entry: python utils/import_check.py
+      language: python
       files: ^narwhals/
       exclude: ^narwhals/dependencies\.py
 - repo: https://github.com/kynan/nbstripout
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 03d80fec..b1eb91b0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -67,6 +67,26 @@ Please adhere to the following guidelines:
 If Narwhals looks like underwater unicorn magic to you, then please read
 [how it works](https://narwhals-dev.github.io/narwhals/how-it-works/).
 
+## Imports
+
+In Narwhals, we are very particular about imports. When it comes to importing
+heavy third-party libraries (pandas, NumPy, Polars, etc...) please follow these rules:
+
+- Never import anything to do `isinstance` checks. Instead, just use the functions
+  in `narwhals.dependencies` (such as `is_pandas_dataframe`);
+- If you need to import anything, do it in a place where you know that the import
+  is definitely available. For example, NumPy is a required dependency of PyArrow,
+  so it's OK to import NumPy to implement a PyArrow function - however, NumPy
+  should never be imported to implement a Polars function. The only exception is
+  for when there's simply no way around it by definition - for example, `Series.to_numpy`
+  always requires NumPy to be installed.
+- Don't place a third-party import at the top of a file. Instead, place it in the
+  function where it's used, so that we minimise the chances of it being imported
+  unnecessarily.
+
+We're trying to be really lightweight and minimal-overhead, and
+unnecessary imports can slow things down.
+
 ## Happy contributing!
 
 Please remember to abide by the code of conduct, else you'll be conducted away from this project.
diff --git a/docs/api-reference/dataframe.md b/docs/api-reference/dataframe.md
index 676f6407..b251b2a5 100644
--- a/docs/api-reference/dataframe.md
+++ b/docs/api-reference/dataframe.md
@@ -4,6 +4,7 @@
     handler: python
     options:
       members:
+        - __arrow_c_stream__
         - __getitem__
         - clone
         - collect_schema
diff --git a/docs/api-reference/narwhals.md b/docs/api-reference/narwhals.md
index 16bc6621..275a865c 100644
--- a/docs/api-reference/narwhals.md
+++ b/docs/api-reference/narwhals.md
@@ -17,14 +17,15 @@ Here are the top-level functions available in Narwhals.
         - get_native_namespace
         - is_ordered_categorical
         - len
-        - maybe_align_index
-        - maybe_set_index
-        - maybe_convert_dtypes
         - lit
         - max
+        - maybe_align_index
+        - maybe_convert_dtypes
+        - maybe_set_index
         - mean
         - min
         - narwhalify
+        - new_series
         - sum
         - sum_horizontal
         - show_versions
diff --git a/docs/api-reference/series.md b/docs/api-reference/series.md
index 7b7f62b8..f9cc2e6b 100644
--- a/docs/api-reference/series.md
+++ b/docs/api-reference/series.md
@@ -4,6 +4,8 @@
     handler: python
     options:
       members:
+        - __arrow_c_stream__
+        - __getitem__
         - abs
         - alias
         - all
diff --git a/docs/extending.md b/docs/extending.md
index dc0d4671..a41eae4f 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -26,14 +26,16 @@ Make sure that, in addition to the public Narwhals API, you also define:
     from `Narwhals.DataFrame`
   - `DataFrame.__narwhals_namespace__`: return an object which implements public top-level
     functions from `narwhals` (e.g. `narwhals.col`, `narwhals.concat`, ...)
+  - `DataFrame.__native_namespace__`: return a native namespace object which must have a
+    `from_dict` method
   - `LazyFrame.__narwhals_lazyframe__`: return an object which implements public methods
     from `Narwhals.LazyFrame`
   - `LazyFrame.__narwhals_namespace__`: return an object which implements public top-level
     functions from `narwhals` (e.g. `narwhals.col`, `narwhals.concat`, ...)
+  - `LazyFrame.__native_namespace__`: return a native namespace object which must have a
+    `from_dict` method
   - `Series.__narwhals_series__`: return an object which implements public methods
     from `Narwhals.Series`
-  - `Series.__narwhals_namespace__`: return an object which implements public top-level
-    functions from `narwhals` (e.g. `narwhals.col`, `narwhals.concat`, ...)
 
   If your library doesn't distinguish between lazy and eager, then it's OK for your dataframe
   object to implement both `__narwhals_dataframe__` and `__narwhals_lazyframe__`. In fact,
diff --git a/docs/how_it_works.md b/docs/how_it_works.md
index b9b28992..cda98a2b 100644
--- a/docs/how_it_works.md
+++ b/docs/how_it_works.md
@@ -88,7 +88,7 @@ The `_call` method gives us that function! Let's see it in action.
 
 Note: the following examples use `PandasLikeDataFrame` and `PandasLikeSeries`. These are backed
 by actual `pandas.DataFrame`s and `pandas.Series` respectively and are Narwhals-compliant. We can access the 
-underlying pandas objects via `PandasLikeDataFrame._native_dataframe` and `PandasLikeSeries._native_series`.
+underlying pandas objects via `PandasLikeDataFrame._native_frame` and `PandasLikeSeries._native_series`.
 
 ```python exec="1" result="python" session="pandas_impl" source="above"
 import narwhals as nw
@@ -222,10 +222,10 @@ df_compliant = df._compliant_frame
 result = df_compliant.select(expr)
 ```
 
-We can then view the underlying pandas Dataframe which was produced by calling `._native_dataframe`:
+We can then view the underlying pandas Dataframe which was produced by calling `._native_frame`:
 
 ```python exec="1" result="python" session="pandas_api_mapping" source="above"
-print(result._native_dataframe)
+print(result._native_frame)
 ```
 which is the same as we'd have obtained by just using the Narwhals API directly:
 
diff --git a/docs/installation.md b/docs/installation.md
index a4ca987d..81f3fad9 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -11,6 +11,6 @@ Then, if you start the Python REPL and see the following:
 ```python
 >>> import narwhals
 >>> narwhals.__version__
-'1.3.0'
+'1.4.1'
 ```
 then installation worked correctly!
diff --git a/narwhals/__init__.py b/narwhals/__init__.py
index 6571e66a..aacd1065 100644
--- a/narwhals/__init__.py
+++ b/narwhals/__init__.py
@@ -36,6 +36,7 @@ from narwhals.expr import sum_horizontal
 from narwhals.functions import concat
 from narwhals.functions import from_dict
 from narwhals.functions import get_level
+from narwhals.functions import new_series
 from narwhals.functions import show_versions
 from narwhals.schema import Schema
 from narwhals.series import Series
@@ -48,13 +49,14 @@ from narwhals.utils import maybe_align_index
 from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_set_index
 
-__version__ = "1.3.0"
+__version__ = "1.4.1"
 
 __all__ = [
     "selectors",
     "concat",
     "from_dict",
     "get_level",
+    "new_series",
     "to_native",
     "from_native",
     "is_ordered_categorical",
diff --git a/narwhals/_arrow/dataframe.py b/narwhals/_arrow/dataframe.py
index 13ff0da0..865d1709 100644
--- a/narwhals/_arrow/dataframe.py
+++ b/narwhals/_arrow/dataframe.py
@@ -12,13 +12,12 @@ from narwhals._arrow.utils import broadcast_series
 from narwhals._arrow.utils import translate_dtype
 from narwhals._arrow.utils import validate_dataframe_comparand
 from narwhals._expression_parsing import evaluate_into_exprs
-from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pyarrow
-from narwhals.dependencies import get_pyarrow_compute
-from narwhals.dependencies import get_pyarrow_parquet
+from narwhals.dependencies import is_numpy_array
 from narwhals.utils import Implementation
 from narwhals.utils import flatten
 from narwhals.utils import generate_unique_token
+from narwhals.utils import parse_columns_to_drop
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -35,7 +34,7 @@ class ArrowDataFrame:
     def __init__(
         self, native_dataframe: Any, *, backend_version: tuple[int, ...]
     ) -> None:
-        self._native_dataframe = native_dataframe
+        self._native_frame = native_dataframe
         self._implementation = Implementation.PYARROW
         self._backend_version = backend_version
 
@@ -53,15 +52,15 @@ class ArrowDataFrame:
     def __narwhals_lazyframe__(self) -> Self:
         return self
 
-    def _from_native_dataframe(self, df: Any) -> Self:
+    def _from_native_frame(self, df: Any) -> Self:
         return self.__class__(df, backend_version=self._backend_version)
 
     @property
     def shape(self) -> tuple[int, int]:
-        return self._native_dataframe.shape  # type: ignore[no-any-return]
+        return self._native_frame.shape  # type: ignore[no-any-return]
 
     def __len__(self) -> int:
-        return len(self._native_dataframe)
+        return len(self._native_frame)
 
     def rows(
         self, *, named: bool = False
@@ -69,7 +68,7 @@ class ArrowDataFrame:
         if not named:
             msg = "Unnamed rows are not yet supported on PyArrow tables"
             raise NotImplementedError(msg)
-        return self._native_dataframe.to_pylist()  # type: ignore[no-any-return]
+        return self._native_frame.to_pylist()  # type: ignore[no-any-return]
 
     def iter_rows(
         self,
@@ -77,7 +76,7 @@ class ArrowDataFrame:
         named: bool = False,
         buffer_size: int = 512,
     ) -> Iterator[tuple[Any, ...]] | Iterator[dict[str, Any]]:
-        df = self._native_dataframe
+        df = self._native_frame
         num_rows = df.num_rows
 
         if not named:
@@ -96,7 +95,7 @@ class ArrowDataFrame:
             raise TypeError(msg)
 
         return ArrowSeries(
-            self._native_dataframe[name],
+            self._native_frame[name],
             name=name,
             backend_version=self._backend_version,
         )
@@ -120,7 +119,7 @@ class ArrowDataFrame:
             from narwhals._arrow.series import ArrowSeries
 
             return ArrowSeries(
-                self._native_dataframe[item],
+                self._native_frame[item],
                 name=item,
                 backend_version=self._backend_version,
             )
@@ -129,8 +128,8 @@ class ArrowDataFrame:
             and len(item) == 2
             and isinstance(item[1], (list, tuple))
         ):
-            return self._from_native_dataframe(
-                self._native_dataframe.take(item[0]).select(item[1])
+            return self._from_native_frame(
+                self._native_frame.take(item[0]).select(item[1])
             )
 
         elif isinstance(item, tuple) and len(item) == 2:
@@ -139,7 +138,7 @@ class ArrowDataFrame:
             # PyArrow columns are always strings
             col_name = item[1] if isinstance(item[1], str) else self.columns[item[1]]
             return ArrowSeries(
-                self._native_dataframe[col_name].take(item[0]),
+                self._native_frame[col_name].take(item[0]),
                 name=col_name,
                 backend_version=self._backend_version,
             )
@@ -149,17 +148,13 @@ class ArrowDataFrame:
                 msg = "Slicing with step is not supported on PyArrow tables"
                 raise NotImplementedError(msg)
             start = item.start or 0
-            stop = item.stop or len(self._native_dataframe)
-            return self._from_native_dataframe(
-                self._native_dataframe.slice(item.start, stop - start),
+            stop = item.stop or len(self._native_frame)
+            return self._from_native_frame(
+                self._native_frame.slice(item.start, stop - start),
             )
 
-        elif isinstance(item, Sequence) or (
-            (np := get_numpy()) is not None
-            and isinstance(item, np.ndarray)
-            and item.ndim == 1
-        ):
-            return self._from_native_dataframe(self._native_dataframe.take(item))
+        elif isinstance(item, Sequence) or (is_numpy_array(item) and item.ndim == 1):
+            return self._from_native_frame(self._native_frame.take(item))
 
         else:  # pragma: no cover
             msg = f"Expected str or slice, got: {type(item)}"
@@ -167,7 +162,7 @@ class ArrowDataFrame:
 
     @property
     def schema(self) -> dict[str, DType]:
-        schema = self._native_dataframe.schema
+        schema = self._native_frame.schema
         return {
             name: translate_dtype(dtype)
             for name, dtype in zip(schema.names, schema.types)
@@ -178,26 +173,25 @@ class ArrowDataFrame:
 
     @property
     def columns(self) -> list[str]:
-        return self._native_dataframe.schema.names  # type: ignore[no-any-return]
+        return self._native_frame.schema.names  # type: ignore[no-any-return]
 
     def select(
         self,
         *exprs: IntoArrowExpr,
         **named_exprs: IntoArrowExpr,
     ) -> Self:
+        import pyarrow as pa  # ignore-banned-import()
+
         new_series = evaluate_into_exprs(self, *exprs, **named_exprs)
         if not new_series:
             # return empty dataframe, like Polars does
-            return self._from_native_dataframe(
-                self._native_dataframe.__class__.from_arrays([])
-            )
+            return self._from_native_frame(self._native_frame.__class__.from_arrays([]))
         names = [s.name for s in new_series]
-        pa = get_pyarrow()
         df = pa.Table.from_arrays(
             broadcast_series(new_series),
             names=names,
         )
-        return self._from_native_dataframe(df)
+        return self._from_native_frame(df)
 
     def with_columns(
         self,
@@ -220,7 +214,7 @@ class ArrowDataFrame:
                     )
                 )
             else:
-                to_concat.append(self._native_dataframe[name])
+                to_concat.append(self._native_frame[name])
             output_names.append(name)
         for s in new_column_name_to_new_column_map:
             to_concat.append(
@@ -231,8 +225,8 @@ class ArrowDataFrame:
                 )
             )
             output_names.append(s)
-        df = self._native_dataframe.__class__.from_arrays(to_concat, names=output_names)
-        return self._from_native_dataframe(df)
+        df = self._native_frame.__class__.from_arrays(to_concat, names=output_names)
+        return self._from_native_frame(df)
 
     def group_by(self, *keys: str) -> ArrowGroupBy:
         from narwhals._arrow.group_by import ArrowGroupBy
@@ -265,19 +259,21 @@ class ArrowDataFrame:
                 n_bytes=8, columns=[*self.columns, *other.columns]
             )
 
-            return self._from_native_dataframe(
-                self.with_columns(**{key_token: plx.lit(0, None)})._native_dataframe.join(
-                    other.with_columns(**{key_token: plx.lit(0, None)})._native_dataframe,
+            return self._from_native_frame(
+                self.with_columns(**{key_token: plx.lit(0, None)})
+                ._native_frame.join(
+                    other.with_columns(**{key_token: plx.lit(0, None)})._native_frame,
                     keys=key_token,
                     right_keys=key_token,
                     join_type="inner",
                     right_suffix="_right",
-                ),
-            ).drop(key_token)
+                )
+                .drop([key_token]),
+            )
 
-        return self._from_native_dataframe(
-            self._native_dataframe.join(
-                other._native_dataframe,
+        return self._from_native_frame(
+            self._native_frame.join(
+                other._native_frame,
                 keys=left_on,
                 right_keys=right_on,
                 join_type=how_to_join_map[how],
@@ -285,12 +281,15 @@ class ArrowDataFrame:
             ),
         )
 
-    def drop(self, *columns: str) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.drop(list(columns)))
+    def drop(self: Self, columns: list[str], strict: bool) -> Self:  # noqa: FBT001
+        to_drop = parse_columns_to_drop(
+            compliant_frame=self, columns=columns, strict=strict
+        )
+        return self._from_native_frame(self._native_frame.drop(to_drop))
 
     def drop_nulls(self: Self, subset: str | list[str] | None) -> Self:
         if subset is None:
-            return self._from_native_dataframe(self._native_dataframe.drop_null())
+            return self._from_native_frame(self._native_frame.drop_null())
         subset = [subset] if isinstance(subset, str) else subset
         plx = self.__narwhals_namespace__()
         return self.filter(~plx.any_horizontal(plx.col(*subset).is_null()))
@@ -302,7 +301,7 @@ class ArrowDataFrame:
         descending: bool | Sequence[bool] = False,
     ) -> Self:
         flat_keys = flatten([*flatten([by]), *more_by])
-        df = self._native_dataframe
+        df = self._native_frame
 
         if isinstance(descending, bool):
             order = "descending" if descending else "ascending"
@@ -312,18 +311,18 @@ class ArrowDataFrame:
                 (key, "descending" if is_descending else "ascending")
                 for key, is_descending in zip(flat_keys, descending)
             ]
-        return self._from_native_dataframe(df.sort_by(sorting=sorting))
+        return self._from_native_frame(df.sort_by(sorting=sorting))
 
     def to_pandas(self) -> Any:
-        return self._native_dataframe.to_pandas()
+        return self._native_frame.to_pandas()
 
     def to_numpy(self) -> Any:
-        import numpy as np
+        import numpy as np  # ignore-banned-import
 
-        return np.column_stack([col.to_numpy() for col in self._native_dataframe.columns])
+        return np.column_stack([col.to_numpy() for col in self._native_frame.columns])
 
     def to_dict(self, *, as_series: bool) -> Any:
-        df = self._native_dataframe
+        df = self._native_frame
 
         names_and_values = zip(df.column_names, df.columns)
         if as_series:
@@ -337,58 +336,54 @@ class ArrowDataFrame:
             return {name: col.to_pylist() for name, col in names_and_values}
 
     def with_row_index(self, name: str) -> Self:
-        pa = get_pyarrow()
-        df = self._native_dataframe
+        import pyarrow as pa  # ignore-banned-import()
+
+        df = self._native_frame
 
         row_indices = pa.array(range(df.num_rows))
-        return self._from_native_dataframe(df.append_column(name, row_indices))
+        return self._from_native_frame(df.append_column(name, row_indices))
 
     def filter(
         self,
         *predicates: IntoArrowExpr,
     ) -> Self:
-        from narwhals._arrow.namespace import ArrowNamespace
-
-        plx = ArrowNamespace(backend_version=self._backend_version)
+        plx = self.__narwhals_namespace__()
         expr = plx.all_horizontal(*predicates)
         # Safety: all_horizontal's expression only returns a single column.
         mask = expr._call(self)[0]
-        return self._from_native_dataframe(
-            self._native_dataframe.filter(mask._native_series)
-        )
+        return self._from_native_frame(self._native_frame.filter(mask._native_series))
 
     def null_count(self) -> Self:
-        pa = get_pyarrow()
-        df = self._native_dataframe
+        import pyarrow as pa  # ignore-banned-import()
+
+        df = self._native_frame
         names_and_values = zip(df.column_names, df.columns)
 
-        return self._from_native_dataframe(
+        return self._from_native_frame(
             pa.table({name: [col.null_count] for name, col in names_and_values})
         )
 
     def head(self, n: int) -> Self:
-        df = self._native_dataframe
+        df = self._native_frame
         if n >= 0:
-            return self._from_native_dataframe(df.slice(0, n))
+            return self._from_native_frame(df.slice(0, n))
         else:
             num_rows = df.num_rows
-            return self._from_native_dataframe(df.slice(0, max(0, num_rows + n)))
+            return self._from_native_frame(df.slice(0, max(0, num_rows + n)))
 
     def tail(self, n: int) -> Self:
-        df = self._native_dataframe
+        df = self._native_frame
         if n >= 0:
             num_rows = df.num_rows
-            return self._from_native_dataframe(df.slice(max(0, num_rows - n)))
+            return self._from_native_frame(df.slice(max(0, num_rows - n)))
         else:
-            return self._from_native_dataframe(df.slice(abs(n)))
+            return self._from_native_frame(df.slice(abs(n)))
 
     def lazy(self) -> Self:
         return self
 
     def collect(self) -> ArrowDataFrame:
-        return ArrowDataFrame(
-            self._native_dataframe, backend_version=self._backend_version
-        )
+        return ArrowDataFrame(self._native_frame, backend_version=self._backend_version)
 
     def clone(self) -> Self:
         msg = "clone is not yet supported on PyArrow tables"
@@ -406,31 +401,33 @@ class ArrowDataFrame:
                     f" frame has shape {self.shape!r}"
                 )
                 raise ValueError(msg)
-            return self._native_dataframe[0][0]
+            return self._native_frame[0][0]
 
         elif row is None or column is None:
             msg = "cannot call `.item()` with only one of `row` or `column`"
             raise ValueError(msg)
 
         _col = self.columns.index(column) if isinstance(column, str) else column
-        return self._native_dataframe[_col][row]
+        return self._native_frame[_col][row]
 
     def rename(self, mapping: dict[str, str]) -> Self:
-        df = self._native_dataframe
+        df = self._native_frame
         new_cols = [mapping.get(c, c) for c in df.column_names]
-        return self._from_native_dataframe(df.rename_columns(new_cols))
+        return self._from_native_frame(df.rename_columns(new_cols))
 
     def write_parquet(self, file: Any) -> Any:
-        pp = get_pyarrow_parquet()
-        pp.write_table(self._native_dataframe, file)
+        import pyarrow.parquet as pp  # ignore-banned-import
+
+        pp.write_table(self._native_frame, file)
 
     def is_duplicated(self: Self) -> ArrowSeries:
+        import numpy as np  # ignore-banned-import
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         from narwhals._arrow.series import ArrowSeries
 
-        np = get_numpy()
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
-        df = self._native_dataframe
+        df = self._native_frame
 
         columns = self.columns
         col_token = generate_unique_token(n_bytes=8, columns=columns)
@@ -448,9 +445,10 @@ class ArrowDataFrame:
         return ArrowSeries(is_duplicated, name="", backend_version=self._backend_version)
 
     def is_unique(self: Self) -> ArrowSeries:
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         from narwhals._arrow.series import ArrowSeries
 
-        pc = get_pyarrow_compute()
         is_duplicated = self.is_duplicated()._native_series
 
         return ArrowSeries(
@@ -469,12 +467,11 @@ class ArrowDataFrame:
             The param `maintain_order` is only here for compatibility with the polars API
             and has no effect on the output.
         """
+        import numpy as np  # ignore-banned-import
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
 
-        np = get_numpy()
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
-
-        df = self._native_dataframe
+        df = self._native_frame
 
         if isinstance(subset, str):
             subset = [subset]
@@ -492,13 +489,13 @@ class ArrowDataFrame:
                 .column(f"{col_token}_{agg_func}")
             )
 
-            return self._from_native_dataframe(pc.take(df, keep_idx))
+            return self._from_native_frame(pc.take(df, keep_idx))
 
         keep_idx = self.select(*subset).is_unique()
         return self.filter(keep_idx)
 
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
-        return self._from_native_dataframe(self._native_dataframe[offset::n])
+        return self._from_native_frame(self._native_frame[offset::n])
 
     def to_arrow(self: Self) -> Any:
-        return self._native_dataframe
+        return self._native_frame
diff --git a/narwhals/_arrow/expr.py b/narwhals/_arrow/expr.py
index d3c2db10..ca9293b8 100644
--- a/narwhals/_arrow/expr.py
+++ b/narwhals/_arrow/expr.py
@@ -56,7 +56,7 @@ class ArrowExpr:
         def func(df: ArrowDataFrame) -> list[ArrowSeries]:
             return [
                 ArrowSeries(
-                    df._native_dataframe[column_name],
+                    df._native_frame[column_name],
                     name=column_name,
                     backend_version=df._backend_version,
                 )
@@ -158,9 +158,7 @@ class ArrowExpr:
         return reuse_series_implementation(self, "len", returns_scalar=True)
 
     def filter(self, *predicates: Any) -> Self:
-        from narwhals._arrow.namespace import ArrowNamespace
-
-        plx = ArrowNamespace(backend_version=self._backend_version)
+        plx = self.__narwhals_namespace__()
         expr = plx.all_horizontal(*predicates)
         return reuse_series_implementation(self, "filter", other=expr)
 
diff --git a/narwhals/_arrow/group_by.py b/narwhals/_arrow/group_by.py
index b05b41b8..27c7ff36 100644
--- a/narwhals/_arrow/group_by.py
+++ b/narwhals/_arrow/group_by.py
@@ -8,8 +8,6 @@ from typing import Iterator
 
 from narwhals._expression_parsing import is_simple_aggregation
 from narwhals._expression_parsing import parse_into_exprs
-from narwhals.dependencies import get_pyarrow
-from narwhals.dependencies import get_pyarrow_compute
 from narwhals.utils import remove_prefix
 
 if TYPE_CHECKING:
@@ -20,10 +18,11 @@ if TYPE_CHECKING:
 
 class ArrowGroupBy:
     def __init__(self, df: ArrowDataFrame, keys: list[str]) -> None:
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         self._df = df
         self._keys = list(keys)
-        self._grouped = pa.TableGroupBy(self._df._native_dataframe, list(self._keys))
+        self._grouped = pa.TableGroupBy(self._df._native_frame, list(self._keys))
 
     def agg(
         self,
@@ -51,7 +50,7 @@ class ArrowGroupBy:
             exprs,
             self._keys,
             output_names,
-            self._df._from_native_dataframe,
+            self._df._from_native_frame,
         )
 
     def __iter__(self) -> Iterator[tuple[Any, ArrowDataFrame]]:
@@ -79,7 +78,8 @@ def agg_arrow(
     output_names: list[str],
     from_dataframe: Callable[[Any], ArrowDataFrame],
 ) -> ArrowDataFrame:
-    pc = get_pyarrow_compute()
+    import pyarrow.compute as pc  # ignore-banned-import()
+
     all_simple_aggs = True
     for expr in exprs:
         if not is_simple_aggregation(expr):
diff --git a/narwhals/_arrow/namespace.py b/narwhals/_arrow/namespace.py
index 57bd5a4f..bb90b179 100644
--- a/narwhals/_arrow/namespace.py
+++ b/narwhals/_arrow/namespace.py
@@ -13,7 +13,6 @@ from narwhals._arrow.series import ArrowSeries
 from narwhals._arrow.utils import horizontal_concat
 from narwhals._arrow.utils import vertical_concat
 from narwhals._expression_parsing import parse_into_exprs
-from narwhals.dependencies import get_pyarrow
 from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
@@ -87,9 +86,10 @@ class ArrowNamespace:
         )
 
     def _create_compliant_series(self, value: Any) -> ArrowSeries:
+        import pyarrow as pa  # ignore-banned-import()
+
         from narwhals._arrow.series import ArrowSeries
 
-        pa = get_pyarrow()
         return ArrowSeries(
             native_series=pa.chunked_array([value]),
             name="",
@@ -114,7 +114,7 @@ class ArrowNamespace:
         return ArrowExpr(  # pragma: no cover
             lambda df: [
                 ArrowSeries._from_iterable(
-                    [len(df._native_dataframe)],
+                    [len(df._native_frame)],
                     name="len",
                     backend_version=self._backend_version,
                 )
@@ -133,7 +133,7 @@ class ArrowNamespace:
         return ArrowExpr(
             lambda df: [
                 ArrowSeries(
-                    df._native_dataframe[column_name],
+                    df._native_frame[column_name],
                     name=column_name,
                     backend_version=df._backend_version,
                 )
@@ -181,7 +181,7 @@ class ArrowNamespace:
         *,
         how: str = "vertical",
     ) -> ArrowDataFrame:
-        dfs: list[Any] = [item._native_dataframe for item in items]
+        dfs: list[Any] = [item._native_frame for item in items]
 
         if how == "horizontal":
             return ArrowDataFrame(
diff --git a/narwhals/_arrow/series.py b/narwhals/_arrow/series.py
index 3f5926d2..fb15f3aa 100644
--- a/narwhals/_arrow/series.py
+++ b/narwhals/_arrow/series.py
@@ -9,13 +9,11 @@ from typing import overload
 
 from narwhals._arrow.utils import cast_for_truediv
 from narwhals._arrow.utils import floordiv_compat
-from narwhals._arrow.utils import reverse_translate_dtype
+from narwhals._arrow.utils import narwhals_to_native_dtype
 from narwhals._arrow.utils import translate_dtype
 from narwhals._arrow.utils import validate_column_comparand
-from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_pyarrow
-from narwhals.dependencies import get_pyarrow_compute
 from narwhals.utils import Implementation
 from narwhals.utils import generate_unique_token
 
@@ -23,7 +21,6 @@ if TYPE_CHECKING:
     from typing_extensions import Self
 
     from narwhals._arrow.dataframe import ArrowDataFrame
-    from narwhals._arrow.namespace import ArrowNamespace
     from narwhals.dtypes import DType
 
 
@@ -37,7 +34,8 @@ class ArrowSeries:
         self._backend_version = backend_version
 
     def _from_native_series(self, series: Any) -> Self:
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         if isinstance(series, pa.Array):
             series = pa.chunked_array([series])
         return self.__class__(
@@ -54,7 +52,8 @@ class ArrowSeries:
         *,
         backend_version: tuple[int, ...],
     ) -> Self:
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         return cls(
             pa.chunked_array([data]),
             name=name,
@@ -65,67 +64,78 @@ class ArrowSeries:
         return len(self._native_series)
 
     def __eq__(self, other: object) -> Self:  # type: ignore[override]
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.equal(ser, other))
 
     def __ne__(self, other: object) -> Self:  # type: ignore[override]
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.not_equal(ser, other))
 
     def __ge__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.greater_equal(ser, other))
 
     def __gt__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.greater(ser, other))
 
     def __le__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.less_equal(ser, other))
 
     def __lt__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.less(ser, other))
 
     def __and__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.and_kleene(ser, other))
 
     def __rand__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.and_kleene(other, ser))
 
     def __or__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.or_kleene(ser, other))
 
     def __ror__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.or_kleene(other, ser))
 
     def __add__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         other = validate_column_comparand(other)
         return self._from_native_series(pc.add(self._native_series, other))
 
@@ -133,7 +143,8 @@ class ArrowSeries:
         return self + other  # type: ignore[no-any-return]
 
     def __sub__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         other = validate_column_comparand(other)
         return self._from_native_series(pc.subtract(self._native_series, other))
 
@@ -141,7 +152,8 @@ class ArrowSeries:
         return (self - other) * (-1)  # type: ignore[no-any-return]
 
     def __mul__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         other = validate_column_comparand(other)
         return self._from_native_series(pc.multiply(self._native_series, other))
 
@@ -149,13 +161,15 @@ class ArrowSeries:
         return self * other  # type: ignore[no-any-return]
 
     def __pow__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.power(ser, other))
 
     def __rpow__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         return self._from_native_series(pc.power(other, ser))
@@ -171,8 +185,9 @@ class ArrowSeries:
         return self._from_native_series(floordiv_compat(other, ser))
 
     def __truediv__(self, other: Any) -> Self:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         if not isinstance(other, (pa.Array, pa.ChunkedArray)):
@@ -181,8 +196,9 @@ class ArrowSeries:
         return self._from_native_series(pc.divide(*cast_for_truediv(ser, other)))
 
     def __rtruediv__(self, other: Any) -> Self:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         if not isinstance(other, (pa.Array, pa.ChunkedArray)):
@@ -191,7 +207,8 @@ class ArrowSeries:
         return self._from_native_series(pc.divide(*cast_for_truediv(other, ser)))
 
     def __mod__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         floor_div = (self // other)._native_series
@@ -199,7 +216,8 @@ class ArrowSeries:
         return self._from_native_series(res)
 
     def __rmod__(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         other = validate_column_comparand(other)
         floor_div = (other // self)._native_series
@@ -207,7 +225,8 @@ class ArrowSeries:
         return self._from_native_series(res)
 
     def __invert__(self) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(pc.invert(self._native_series))
 
     def len(self) -> int:
@@ -218,27 +237,33 @@ class ArrowSeries:
         return self._from_native_series(self._native_series.filter(other))
 
     def mean(self) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.mean(self._native_series)  # type: ignore[no-any-return]
 
     def min(self) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.min(self._native_series)  # type: ignore[no-any-return]
 
     def max(self) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.max(self._native_series)  # type: ignore[no-any-return]
 
     def sum(self) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.sum(self._native_series)  # type: ignore[no-any-return]
 
     def drop_nulls(self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(pc.drop_null(self._native_series))
 
     def shift(self, n: int) -> Self:
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         ca = self._native_series
 
         if n > 0:
@@ -250,26 +275,24 @@ class ArrowSeries:
         return self._from_native_series(result)
 
     def std(self, ddof: int = 1) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.stddev(self._native_series, ddof=ddof)  # type: ignore[no-any-return]
 
     def count(self) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.count(self._native_series)  # type: ignore[no-any-return]
 
     def n_unique(self) -> int:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         unique_values = pc.unique(self._native_series)
         return pc.count(unique_values, mode="all")  # type: ignore[no-any-return]
 
     def __native_namespace__(self) -> Any:  # pragma: no cover
         return get_pyarrow()
 
-    def __narwhals_namespace__(self) -> ArrowNamespace:
-        from narwhals._arrow.namespace import ArrowNamespace
-
-        return ArrowNamespace(backend_version=self._backend_version)
-
     @property
     def name(self) -> str:
         return self._name
@@ -309,35 +332,42 @@ class ArrowSeries:
         return translate_dtype(self._native_series.type)
 
     def abs(self) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(pc.abs(self._native_series))
 
     def cum_sum(self) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(pc.cumulative_sum(self._native_series))
 
     def round(self, decimals: int) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(
             pc.round(self._native_series, decimals, round_mode="half_towards_infinity")
         )
 
     def diff(self) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(
             pc.pairwise_diff(self._native_series.combine_chunks())
         )
 
     def any(self) -> bool:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.any(self._native_series)  # type: ignore[no-any-return]
 
     def all(self) -> bool:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.all(self._native_series)  # type: ignore[no-any-return]
 
     def is_between(self, lower_bound: Any, upper_bound: Any, closed: str = "both") -> Any:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         if closed == "left":
             ge = pc.greater_equal(ser, lower_bound)
@@ -367,9 +397,10 @@ class ArrowSeries:
         return self._from_native_series(ser.is_null())
 
     def cast(self, dtype: DType) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
-        dtype = reverse_translate_dtype(dtype)
+        dtype = narwhals_to_native_dtype(dtype)
         return self._from_native_series(pc.cast(ser, dtype))
 
     def null_count(self: Self) -> int:
@@ -392,14 +423,16 @@ class ArrowSeries:
             return self._from_native_series(ser.slice(abs(n)))
 
     def is_in(self, other: Any) -> Self:
-        pc = get_pyarrow_compute()
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         value_set = pa.array(other)
         ser = self._native_series
         return self._from_native_series(pc.is_in(ser, value_set=value_set))
 
     def arg_true(self) -> Self:
-        np = get_numpy()
+        import numpy as np  # ignore-banned-import
+
         ser = self._native_series
         res = np.flatnonzero(ser)
         return self._from_iterable(
@@ -426,10 +459,10 @@ class ArrowSeries:
         normalize: bool = False,
     ) -> ArrowDataFrame:
         """Parallel is unused, exists for compatibility"""
-        from narwhals._arrow.dataframe import ArrowDataFrame
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
 
-        pc = get_pyarrow_compute()
-        pa = get_pyarrow()
+        from narwhals._arrow.dataframe import ArrowDataFrame
 
         index_name_ = "index" if self._name is None else self._name
         value_name_ = name or ("proportion" if normalize else "count")
@@ -454,7 +487,7 @@ class ArrowSeries:
         )
 
     def zip_with(self: Self, mask: Self, other: Self) -> Self:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
 
         return self._from_native_series(
             pc.replace_with_mask(
@@ -471,8 +504,9 @@ class ArrowSeries:
         *,
         with_replacement: bool = False,
     ) -> Self:
-        np = get_numpy()
-        pc = get_pyarrow_compute()
+        import numpy as np  # ignore-banned-import
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         num_rows = len(self)
 
@@ -484,17 +518,19 @@ class ArrowSeries:
         return self._from_native_series(pc.take(ser, mask))
 
     def fill_null(self: Self, value: Any) -> Self:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         dtype = ser.type
 
         return self._from_native_series(pc.fill_null(ser, pa.scalar(value, dtype)))
 
     def to_frame(self: Self) -> ArrowDataFrame:
+        import pyarrow as pa  # ignore-banned-import()
+
         from narwhals._arrow.dataframe import ArrowDataFrame
 
-        pa = get_pyarrow()
         df = pa.Table.from_arrays([self._native_series], names=[self.name])
         return ArrowDataFrame(df, backend_version=self._backend_version)
 
@@ -509,9 +545,9 @@ class ArrowSeries:
         return self.to_frame().is_unique().alias(self.name)
 
     def is_first_distinct(self: Self) -> Self:
-        np = get_numpy()
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import numpy as np  # ignore-banned-import
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
 
         row_number = pa.array(np.arange(len(self)))
         col_token = generate_unique_token(n_bytes=8, columns=[self.name])
@@ -526,9 +562,9 @@ class ArrowSeries:
         return self._from_native_series(pc.is_in(row_number, first_distinct_index))
 
     def is_last_distinct(self: Self) -> Self:
-        np = get_numpy()
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import numpy as np  # ignore-banned-import
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
 
         row_number = pa.array(np.arange(len(self)))
         col_token = generate_unique_token(n_bytes=8, columns=[self.name])
@@ -546,7 +582,8 @@ class ArrowSeries:
         if not isinstance(descending, bool):
             msg = f"argument 'descending' should be boolean, found {type(descending)}"
             raise TypeError(msg)
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         ser = self._native_series
         if descending:
             return pc.all(pc.greater_equal(ser[:-1], ser[1:]))  # type: ignore[no-any-return]
@@ -554,13 +591,15 @@ class ArrowSeries:
             return pc.all(pc.less_equal(ser[:-1], ser[1:]))  # type: ignore[no-any-return]
 
     def unique(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._from_native_series(pc.unique(self._native_series))
 
     def sort(
         self: Self, *, descending: bool = False, nulls_last: bool = False
     ) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         series = self._native_series
         order = "descending" if descending else "ascending"
         null_placement = "at_end" if nulls_last else "at_start"
@@ -573,10 +612,10 @@ class ArrowSeries:
     def to_dummies(
         self: Self, *, separator: str = "_", drop_first: bool = False
     ) -> ArrowDataFrame:
-        from narwhals._arrow.dataframe import ArrowDataFrame
+        import numpy as np  # ignore-banned-import
+        import pyarrow as pa  # ignore-banned-import()
 
-        np = get_numpy()
-        pa = get_pyarrow()
+        from narwhals._arrow.dataframe import ArrowDataFrame
 
         series = self._native_series
         da = series.dictionary_encode().combine_chunks()
@@ -595,7 +634,8 @@ class ArrowSeries:
         quantile: float,
         interpolation: Literal["nearest", "higher", "lower", "midpoint", "linear"],
     ) -> Any:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return pc.quantile(self._native_series, q=quantile, interpolation=interpolation)[
             0
         ]
@@ -606,8 +646,8 @@ class ArrowSeries:
     def clip(
         self: Self, lower_bound: Any | None = None, upper_bound: Any | None = None
     ) -> Self:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
 
         arr = self._native_series
         arr = pc.max_element_wise(arr, pa.scalar(lower_bound, type=arr.type))
@@ -640,7 +680,8 @@ class ArrowSeriesDateTimeNamespace:
         self._arrow_series = series
 
     def to_string(self: Self, format: str) -> ArrowSeries:  # noqa: A002
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         # PyArrow differs from other libraries in that %S also prints out
         # the fractional part of the second...:'(
         # https://arrow.apache.org/docs/python/generated/pyarrow.compute.strftime.html
@@ -650,63 +691,72 @@ class ArrowSeriesDateTimeNamespace:
         )
 
     def date(self: Self) -> ArrowSeries:
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             self._arrow_series._native_series.cast(pa.date64())
         )
 
     def year(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.year(self._arrow_series._native_series)
         )
 
     def month(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.month(self._arrow_series._native_series)
         )
 
     def day(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.day(self._arrow_series._native_series)
         )
 
     def hour(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.hour(self._arrow_series._native_series)
         )
 
     def minute(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.minute(self._arrow_series._native_series)
         )
 
     def second(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.second(self._arrow_series._native_series)
         )
 
     def millisecond(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.millisecond(self._arrow_series._native_series)
         )
 
     def microsecond(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         result = pc.add(pc.multiply(pc.millisecond(arr), 1000), pc.microsecond(arr))
 
         return self._arrow_series._from_native_series(result)
 
     def nanosecond(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         result = pc.add(
             pc.multiply(self.microsecond()._native_series, 1000), pc.nanosecond(arr)
@@ -714,14 +764,16 @@ class ArrowSeriesDateTimeNamespace:
         return self._arrow_series._from_native_series(result)
 
     def ordinal_day(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.day_of_year(self._arrow_series._native_series)
         )
 
     def total_minutes(self: Self) -> ArrowSeries:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         unit = arr.type.unit
 
@@ -738,8 +790,9 @@ class ArrowSeriesDateTimeNamespace:
         )
 
     def total_seconds(self: Self) -> ArrowSeries:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         unit = arr.type.unit
 
@@ -756,8 +809,9 @@ class ArrowSeriesDateTimeNamespace:
         )
 
     def total_milliseconds(self: Self) -> ArrowSeries:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         unit = arr.type.unit
 
@@ -780,8 +834,9 @@ class ArrowSeriesDateTimeNamespace:
         )
 
     def total_microseconds(self: Self) -> ArrowSeries:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         unit = arr.type.unit
 
@@ -803,8 +858,9 @@ class ArrowSeriesDateTimeNamespace:
         )
 
     def total_nanoseconds(self: Self) -> ArrowSeries:
-        pa = get_pyarrow()
-        pc = get_pyarrow_compute()
+        import pyarrow as pa  # ignore-banned-import()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         arr = self._arrow_series._native_series
         unit = arr.type.unit
 
@@ -827,7 +883,8 @@ class ArrowSeriesCatNamespace:
         self._arrow_series = series
 
     def get_categories(self) -> ArrowSeries:
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         ca = self._arrow_series._native_series
         # TODO(Unassigned): this looks potentially expensive - is there no better way?
         out = pa.chunked_array(
@@ -843,7 +900,8 @@ class ArrowSeriesStringNamespace:
     def replace(
         self, pattern: str, value: str, *, literal: bool = False, n: int = 1
     ) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         method = "replace_substring" if literal else "replace_substring_regex"
         return self._arrow_series._from_native_series(
             getattr(pc, method)(
@@ -860,7 +918,8 @@ class ArrowSeriesStringNamespace:
         return self.replace(pattern, value, literal=literal, n=-1)
 
     def strip_chars(self: Self, characters: str | None = None) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         whitespace = " \t\n\r\v\f"
         return self._arrow_series._from_native_series(
             pc.utf8_trim(
@@ -870,26 +929,30 @@ class ArrowSeriesStringNamespace:
         )
 
     def starts_with(self: Self, prefix: str) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.equal(self.slice(0, len(prefix))._native_series, prefix)
         )
 
     def ends_with(self: Self, suffix: str) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.equal(self.slice(-len(suffix))._native_series, suffix)
         )
 
     def contains(self: Self, pattern: str, *, literal: bool = False) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         check_func = pc.match_substring if literal else pc.match_substring_regex
         return self._arrow_series._from_native_series(
             check_func(self._arrow_series._native_series, pattern)
         )
 
     def slice(self: Self, offset: int, length: int | None = None) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         stop = offset + length if length else None
         return self._arrow_series._from_native_series(
             pc.utf8_slice_codeunits(
@@ -898,19 +961,22 @@ class ArrowSeriesStringNamespace:
         )
 
     def to_datetime(self: Self, format: str | None = None) -> ArrowSeries:  # noqa: A002
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.strptime(self._arrow_series._native_series, format=format, unit="us")
         )
 
     def to_uppercase(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.utf8_upper(self._arrow_series._native_series),
         )
 
     def to_lowercase(self: Self) -> ArrowSeries:
-        pc = get_pyarrow_compute()
+        import pyarrow.compute as pc  # ignore-banned-import()
+
         return self._arrow_series._from_native_series(
             pc.utf8_lower(self._arrow_series._native_series),
         )
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index 465af61d..6f7517ae 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -4,8 +4,6 @@ from typing import TYPE_CHECKING
 from typing import Any
 
 from narwhals import dtypes
-from narwhals.dependencies import get_pyarrow
-from narwhals.dependencies import get_pyarrow_compute
 from narwhals.utils import isinstance_or_issubclass
 
 if TYPE_CHECKING:
@@ -13,7 +11,8 @@ if TYPE_CHECKING:
 
 
 def translate_dtype(dtype: Any) -> dtypes.DType:
-    pa = get_pyarrow()
+    import pyarrow as pa  # ignore-banned-import()
+
     if pa.types.is_int64(dtype):
         return dtypes.Int64()
     if pa.types.is_int32(dtype):
@@ -55,10 +54,10 @@ def translate_dtype(dtype: Any) -> dtypes.DType:
     raise AssertionError
 
 
-def reverse_translate_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
-    from narwhals import dtypes
+def narwhals_to_native_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
+    import pyarrow as pa  # ignore-banned-import()
 
-    pa = get_pyarrow()
+    from narwhals import dtypes
 
     if isinstance_or_issubclass(dtype, dtypes.Float64):
         return pa.float64()
@@ -143,7 +142,8 @@ def validate_dataframe_comparand(
         return NotImplemented
     if isinstance(other, ArrowSeries):
         if len(other) == 1:
-            pa = get_pyarrow()
+            import pyarrow as pa  # ignore-banned-import()
+
             value = other.item()
             if backend_version < (13,) and hasattr(value, "as_py"):  # pragma: no cover
                 value = value.as_py()
@@ -159,7 +159,8 @@ def horizontal_concat(dfs: list[Any]) -> Any:
 
     Should be in namespace.
     """
-    pa = get_pyarrow()
+    import pyarrow as pa  # ignore-banned-import()
+
     if not dfs:
         msg = "No dataframes to concatenate"  # pragma: no cover
         raise AssertionError(msg)
@@ -191,15 +192,16 @@ def vertical_concat(dfs: list[Any]) -> Any:
             msg = "unable to vstack, column names don't match"
             raise TypeError(msg)
 
-    pa = get_pyarrow()
+    import pyarrow as pa  # ignore-banned-import()
+
     return pa.concat_tables(dfs).combine_chunks()
 
 
 def floordiv_compat(left: Any, right: Any) -> Any:
     # The following lines are adapted from pandas' pyarrow implementation.
     # Ref: https://github.com/pandas-dev/pandas/blob/262fcfbffcee5c3116e86a951d8b693f90411e68/pandas/core/arrays/arrow/array.py#L124-L154
-    pc = get_pyarrow_compute()
-    pa = get_pyarrow()
+    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow.compute as pc  # ignore-banned-import()
 
     if isinstance(left, (int, float)):
         left = pa.scalar(left)
@@ -237,8 +239,8 @@ def floordiv_compat(left: Any, right: Any) -> Any:
 def cast_for_truediv(arrow_array: Any, pa_object: Any) -> tuple[Any, Any]:
     # Lifted from:
     # https://github.com/pandas-dev/pandas/blob/262fcfbffcee5c3116e86a951d8b693f90411e68/pandas/core/arrays/arrow/array.py#L108-L122
-    pc = get_pyarrow_compute()
-    pa = get_pyarrow()
+    import pyarrow as pa  # ignore-banned-import()
+    import pyarrow.compute as pc  # ignore-banned-import()
 
     # Ensure int / int -> float mirroring Python/Numpy behavior
     # as pc.divide_checked(int, int) -> int
@@ -260,7 +262,7 @@ def broadcast_series(series: list[ArrowSeries]) -> list[Any]:
     if fast_path:
         return [s._native_series for s in series]
 
-    pa = get_pyarrow()
+    import pyarrow as pa  # ignore-banned-import()
 
     reshaped = []
     for s, length in zip(series, lengths):
diff --git a/narwhals/_dask/dataframe.py b/narwhals/_dask/dataframe.py
index 6774f762..35ef28bb 100644
--- a/narwhals/_dask/dataframe.py
+++ b/narwhals/_dask/dataframe.py
@@ -14,6 +14,7 @@ from narwhals.dependencies import get_pandas
 from narwhals.utils import Implementation
 from narwhals.utils import flatten
 from narwhals.utils import generate_unique_token
+from narwhals.utils import parse_columns_to_drop
 from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
@@ -29,8 +30,9 @@ class DaskLazyFrame:
     def __init__(
         self, native_dataframe: Any, *, backend_version: tuple[int, ...]
     ) -> None:
-        self._native_dataframe = native_dataframe
+        self._native_frame = native_dataframe
         self._backend_version = backend_version
+        self._implementation = Implementation.DASK
 
     def __native_namespace__(self) -> Any:  # pragma: no cover
         return get_dask_dataframe()
@@ -43,19 +45,19 @@ class DaskLazyFrame:
     def __narwhals_lazyframe__(self) -> Self:
         return self
 
-    def _from_native_dataframe(self, df: Any) -> Self:
+    def _from_native_frame(self, df: Any) -> Self:
         return self.__class__(df, backend_version=self._backend_version)
 
     def with_columns(self, *exprs: DaskExpr, **named_exprs: DaskExpr) -> Self:
-        df = self._native_dataframe
+        df = self._native_frame
         new_series = parse_exprs_and_named_exprs(self, *exprs, **named_exprs)
         df = df.assign(**new_series)
-        return self._from_native_dataframe(df)
+        return self._from_native_frame(df)
 
     def collect(self) -> Any:
         from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
-        result = self._native_dataframe.compute()
+        result = self._native_frame.compute()
         return PandasLikeDataFrame(
             result,
             implementation=Implementation.PANDAS,
@@ -64,7 +66,7 @@ class DaskLazyFrame:
 
     @property
     def columns(self) -> list[str]:
-        return self._native_dataframe.columns.tolist()  # type: ignore[no-any-return]
+        return self._native_frame.columns.tolist()  # type: ignore[no-any-return]
 
     def filter(
         self,
@@ -76,7 +78,7 @@ class DaskLazyFrame:
         expr = plx.all_horizontal(*predicates)
         # Safety: all_horizontal's expression only returns a single column.
         mask = expr._call(self)[0]
-        return self._from_native_dataframe(self._native_dataframe.loc[mask])
+        return self._from_native_frame(self._native_frame.loc[mask])
 
     def lazy(self) -> Self:
         return self
@@ -86,21 +88,20 @@ class DaskLazyFrame:
         *exprs: IntoDaskExpr,
         **named_exprs: IntoDaskExpr,
     ) -> Self:
-        dd = get_dask_dataframe()
+        import dask.dataframe as dd  # ignore-banned-import
 
         if exprs and all(isinstance(x, str) for x in exprs) and not named_exprs:
             # This is a simple slice => fastpath!
-            return self._from_native_dataframe(self._native_dataframe.loc[:, exprs])
+            return self._from_native_frame(self._native_frame.loc[:, exprs])
 
         new_series = parse_exprs_and_named_exprs(self, *exprs, **named_exprs)
 
         if not new_series:
             # return empty dataframe, like Polars does
-            pd = get_pandas()
-            return self._from_native_dataframe(
-                dd.from_pandas(
-                    pd.DataFrame(), npartitions=self._native_dataframe.npartitions
-                )
+            import pandas as pd  # ignore-banned-import
+
+            return self._from_native_frame(
+                dd.from_pandas(pd.DataFrame(), npartitions=self._native_frame.npartitions)
             )
 
         if all(getattr(expr, "_returns_scalar", False) for expr in exprs) and all(
@@ -109,14 +110,14 @@ class DaskLazyFrame:
             df = dd.concat(
                 [val.to_series().rename(name) for name, val in new_series.items()], axis=1
             )
-            return self._from_native_dataframe(df)
+            return self._from_native_frame(df)
 
-        df = self._native_dataframe.assign(**new_series).loc[:, list(new_series.keys())]
-        return self._from_native_dataframe(df)
+        df = self._native_frame.assign(**new_series).loc[:, list(new_series.keys())]
+        return self._from_native_frame(df)
 
     def drop_nulls(self: Self, subset: str | list[str] | None) -> Self:
         if subset is None:
-            return self._from_native_dataframe(self._native_dataframe.dropna())
+            return self._from_native_frame(self._native_frame.dropna())
         subset = [subset] if isinstance(subset, str) else subset
         plx = self.__narwhals_namespace__()
         return self.filter(~plx.any_horizontal(plx.col(*subset).is_null()))
@@ -124,29 +125,31 @@ class DaskLazyFrame:
     @property
     def schema(self) -> dict[str, DType]:
         return {
-            col: translate_dtype(self._native_dataframe.loc[:, col])
-            for col in self._native_dataframe.columns
+            col: translate_dtype(self._native_frame.loc[:, col])
+            for col in self._native_frame.columns
         }
 
     def collect_schema(self) -> dict[str, DType]:
         return self.schema
 
-    def drop(self: Self, *columns: str) -> Self:
-        return self._from_native_dataframe(
-            self._native_dataframe.drop(columns=list(columns))
+    def drop(self: Self, columns: list[str], strict: bool) -> Self:  # noqa: FBT001
+        to_drop = parse_columns_to_drop(
+            compliant_frame=self, columns=columns, strict=strict
         )
 
+        return self._from_native_frame(self._native_frame.drop(columns=to_drop))
+
     def with_row_index(self: Self, name: str) -> Self:
         # Implementation is based on the following StackOverflow reply:
         # https://stackoverflow.com/questions/60831518/in-dask-how-does-one-add-a-range-of-integersauto-increment-to-a-new-column/60852409#60852409
-        return self._from_native_dataframe(add_row_index(self._native_dataframe, name))
+        return self._from_native_frame(add_row_index(self._native_frame, name))
 
     def rename(self: Self, mapping: dict[str, str]) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.rename(columns=mapping))
+        return self._from_native_frame(self._native_frame.rename(columns=mapping))
 
     def head(self: Self, n: int) -> Self:
-        return self._from_native_dataframe(
-            self._native_dataframe.head(n=n, compute=False, npartitions=-1)
+        return self._from_native_frame(
+            self._native_frame.head(n=n, compute=False, npartitions=-1)
         )
 
     def unique(
@@ -162,7 +165,7 @@ class DaskLazyFrame:
             and has no effect on the output.
         """
         subset = flatten(subset) if subset else None
-        native_frame = self._native_dataframe
+        native_frame = self._native_frame
         if keep == "none":
             subset = subset or self.columns
             token = generate_unique_token(n_bytes=8, columns=subset)
@@ -173,7 +176,7 @@ class DaskLazyFrame:
         else:
             mapped_keep = {"any": "first"}.get(keep, keep)
             result = native_frame.drop_duplicates(subset=subset, keep=mapped_keep)
-        return self._from_native_dataframe(result)
+        return self._from_native_frame(result)
 
     def sort(
         self: Self,
@@ -182,12 +185,12 @@ class DaskLazyFrame:
         descending: bool | Sequence[bool] = False,
     ) -> Self:
         flat_keys = flatten([*flatten([by]), *more_by])
-        df = self._native_dataframe
+        df = self._native_frame
         if isinstance(descending, bool):
             ascending: bool | list[bool] = not descending
         else:
             ascending = [not d for d in descending]
-        return self._from_native_dataframe(df.sort_values(flat_keys, ascending=ascending))
+        return self._from_native_frame(df.sort_values(flat_keys, ascending=ascending))
 
     def join(
         self: Self,
@@ -207,15 +210,17 @@ class DaskLazyFrame:
                 n_bytes=8, columns=[*self.columns, *other.columns]
             )
 
-            return self._from_native_dataframe(
-                self._native_dataframe.assign(**{key_token: 0}).merge(
-                    other._native_dataframe.assign(**{key_token: 0}),
+            return self._from_native_frame(
+                self._native_frame.assign(**{key_token: 0})
+                .merge(
+                    other._native_frame.assign(**{key_token: 0}),
                     how="inner",
                     left_on=key_token,
                     right_on=key_token,
                     suffixes=("", "_right"),
-                ),
-            ).drop(key_token)
+                )
+                .drop(columns=key_token),
+            )
 
         if how == "anti":
             indicator_token = generate_unique_token(
@@ -223,33 +228,33 @@ class DaskLazyFrame:
             )
 
             other_native = (
-                other._native_dataframe.loc[:, right_on]
+                other._native_frame.loc[:, right_on]
                 .rename(  # rename to avoid creating extra columns in join
                     columns=dict(zip(right_on, left_on))  # type: ignore[arg-type]
                 )
                 .drop_duplicates()
             )
-            df = self._native_dataframe.merge(
+            df = self._native_frame.merge(
                 other_native,
                 how="outer",
                 indicator=indicator_token,
                 left_on=left_on,
                 right_on=left_on,
             )
-            return self._from_native_dataframe(
+            return self._from_native_frame(
                 df.loc[df[indicator_token] == "left_only"].drop(columns=[indicator_token])
             )
 
         if how == "semi":
             other_native = (
-                other._native_dataframe.loc[:, right_on]
+                other._native_frame.loc[:, right_on]
                 .rename(  # rename to avoid creating extra columns in join
                     columns=dict(zip(right_on, left_on))  # type: ignore[arg-type]
                 )
                 .drop_duplicates()  # avoids potential rows duplication from inner join
             )
-            return self._from_native_dataframe(
-                self._native_dataframe.merge(
+            return self._from_native_frame(
+                self._native_frame.merge(
                     other_native,
                     how="inner",
                     left_on=left_on,
@@ -258,8 +263,8 @@ class DaskLazyFrame:
             )
 
         if how == "left":
-            other_native = other._native_dataframe
-            result_native = self._native_dataframe.merge(
+            other_native = other._native_frame
+            result_native = self._native_frame.merge(
                 other_native,
                 how="left",
                 left_on=left_on,
@@ -272,11 +277,11 @@ class DaskLazyFrame:
                     extra.append(right_key)
                 elif right_key != left_key:
                     extra.append(f"{right_key}_right")
-            return self._from_native_dataframe(result_native.drop(columns=extra))
+            return self._from_native_frame(result_native.drop(columns=extra))
 
-        return self._from_native_dataframe(
-            self._native_dataframe.merge(
-                other._native_dataframe,
+        return self._from_native_frame(
+            self._native_frame.merge(
+                other._native_frame,
                 left_on=left_on,
                 right_on=right_on,
                 how=how,
@@ -290,9 +295,7 @@ class DaskLazyFrame:
         return DaskLazyGroupBy(self, list(by))
 
     def tail(self: Self, n: int) -> Self:
-        return self._from_native_dataframe(
-            self._native_dataframe.tail(n=n, compute=False)
-        )
+        return self._from_native_frame(self._native_frame.tail(n=n, compute=False))
 
     def gather_every(self: Self, n: int, offset: int) -> Self:
         row_index_token = generate_unique_token(n_bytes=8, columns=self.columns)
@@ -303,5 +306,5 @@ class DaskLazyFrame:
                 pln.col(row_index_token) >= offset,  # type: ignore[operator]
                 (pln.col(row_index_token) - offset) % n == 0,  # type: ignore[arg-type]
             )
-            .drop(row_index_token)
+            .drop([row_index_token], strict=False)
         )
diff --git a/narwhals/_dask/expr.py b/narwhals/_dask/expr.py
index f480575c..62dc1646 100644
--- a/narwhals/_dask/expr.py
+++ b/narwhals/_dask/expr.py
@@ -44,6 +44,7 @@ class DaskExpr:
     def __narwhals_expr__(self) -> None: ...
 
     def __narwhals_namespace__(self) -> DaskNamespace:  # pragma: no cover
+        # Unused, just for compatibility with PandasLikeExpr
         from narwhals._dask.namespace import DaskNamespace
 
         return DaskNamespace(backend_version=self._backend_version)
@@ -55,9 +56,7 @@ class DaskExpr:
         backend_version: tuple[int, ...],
     ) -> Self:
         def func(df: DaskLazyFrame) -> list[Any]:
-            return [
-                df._native_dataframe.loc[:, column_name] for column_name in column_names
-            ]
+            return [df._native_frame.loc[:, column_name] for column_name in column_names]
 
         return cls(
             func,
diff --git a/narwhals/_dask/group_by.py b/narwhals/_dask/group_by.py
index 57e112c8..8538c62d 100644
--- a/narwhals/_dask/group_by.py
+++ b/narwhals/_dask/group_by.py
@@ -23,7 +23,7 @@ class DaskLazyGroupBy:
     def __init__(self, df: DaskLazyFrame, keys: list[str]) -> None:
         self._df = df
         self._keys = keys
-        self._grouped = self._df._native_dataframe.groupby(
+        self._grouped = self._df._native_frame.groupby(
             list(self._keys),
             dropna=False,
         )
@@ -54,10 +54,10 @@ class DaskLazyGroupBy:
             self._grouped,
             exprs,
             self._keys,
-            self._from_native_dataframe,
+            self._from_native_frame,
         )
 
-    def _from_native_dataframe(self, df: DaskLazyFrame) -> DaskLazyFrame:
+    def _from_native_frame(self, df: DaskLazyFrame) -> DaskLazyFrame:
         from narwhals._dask.dataframe import DaskLazyFrame
 
         return DaskLazyFrame(
diff --git a/narwhals/_dask/namespace.py b/narwhals/_dask/namespace.py
index edd2eb49..2baf1cf3 100644
--- a/narwhals/_dask/namespace.py
+++ b/narwhals/_dask/namespace.py
@@ -10,8 +10,6 @@ from narwhals import dtypes
 from narwhals._dask.expr import DaskExpr
 from narwhals._dask.selectors import DaskSelectorNamespace
 from narwhals._expression_parsing import parse_into_exprs
-from narwhals.dependencies import get_dask_dataframe
-from narwhals.dependencies import get_pandas
 
 if TYPE_CHECKING:
     from narwhals._dask.dataframe import DaskLazyFrame
@@ -48,9 +46,7 @@ class DaskNamespace:
 
     def all(self) -> DaskExpr:
         def func(df: DaskLazyFrame) -> list[Any]:
-            return [
-                df._native_dataframe.loc[:, column_name] for column_name in df.columns
-            ]
+            return [df._native_frame.loc[:, column_name] for column_name in df.columns]
 
         return DaskExpr(
             func,
@@ -69,10 +65,10 @@ class DaskNamespace:
         )
 
     def lit(self, value: Any, dtype: dtypes.DType | None) -> DaskExpr:
-        # TODO @FBruzzesi: cast to dtype once `reverse_translate_dtype` is implemented.
-        # It should be enough to add `.astype(reverse_translate_dtype(dtype))`
+        # TODO @FBruzzesi: cast to dtype once `narwhals_to_native_dtype` is implemented.
+        # It should be enough to add `.astype(narwhals_to_native_dtype(dtype))`
         return DaskExpr(
-            lambda df: [df._native_dataframe.assign(lit=value).loc[:, "lit"]],
+            lambda df: [df._native_frame.assign(lit=value).loc[:, "lit"]],
             depth=0,
             function_name="lit",
             root_names=None,
@@ -106,20 +102,18 @@ class DaskNamespace:
         ).sum()
 
     def len(self) -> DaskExpr:
-        pd = get_pandas()
-        dd = get_dask_dataframe()
+        import dask.dataframe as dd  # ignore-banned-import
+        import pandas as pd  # ignore-banned-import
 
         def func(df: DaskLazyFrame) -> list[Any]:
             if not df.columns:
                 return [
                     dd.from_pandas(
                         pd.Series([0], name="len"),
-                        npartitions=df._native_dataframe.npartitions,
+                        npartitions=df._native_frame.npartitions,
                     )
                 ]
-            return [
-                df._native_dataframe.loc[:, df.columns[0]].size.to_series().rename("len")
-            ]
+            return [df._native_frame.loc[:, df.columns[0]].size.to_series().rename("len")]
 
         # coverage bug? this is definitely hit
         return DaskExpr(  # pragma: no cover
diff --git a/narwhals/_dask/selectors.py b/narwhals/_dask/selectors.py
index 11d0f0c7..073b3abd 100644
--- a/narwhals/_dask/selectors.py
+++ b/narwhals/_dask/selectors.py
@@ -21,9 +21,7 @@ class DaskSelectorNamespace:
     def by_dtype(self: Self, dtypes: list[DType | type[DType]]) -> DaskSelector:
         def func(df: DaskLazyFrame) -> list[Any]:
             return [
-                df._native_dataframe[col]
-                for col in df.columns
-                if df.schema[col] in dtypes
+                df._native_frame[col] for col in df.columns if df.schema[col] in dtypes
             ]
 
         return DaskSelector(
@@ -63,7 +61,7 @@ class DaskSelectorNamespace:
 
     def all(self: Self) -> DaskSelector:
         def func(df: DaskLazyFrame) -> list[Any]:
-            return [df._native_dataframe[col] for col in df.columns]
+            return [df._native_frame[col] for col in df.columns]
 
         return DaskSelector(
             func,
diff --git a/narwhals/_dask/utils.py b/narwhals/_dask/utils.py
index c0f77e2c..27f0d764 100644
--- a/narwhals/_dask/utils.py
+++ b/narwhals/_dask/utils.py
@@ -19,7 +19,7 @@ def maybe_evaluate(df: DaskLazyFrame, obj: Any) -> Any:
             raise NotImplementedError(msg)
         result = results[0]
         if not get_dask_expr()._expr.are_co_aligned(
-            df._native_dataframe._expr, result._expr
+            df._native_frame._expr, result._expr
         ):  # pragma: no cover
             # are_co_aligned is a method which cheaply checks if two Dask expressions
             # have the same index, and therefore don't require index alignment.
@@ -48,7 +48,7 @@ def parse_exprs_and_named_exprs(
         if hasattr(expr, "__narwhals_expr__"):
             _results = expr._call(df)
         elif isinstance(expr, str):
-            _results = [df._native_dataframe.loc[:, expr]]
+            _results = [df._native_frame.loc[:, expr]]
         else:  # pragma: no cover
             msg = f"Expected expression or column name, got: {expr}"
             raise TypeError(msg)
diff --git a/narwhals/_exceptions.py b/narwhals/_exceptions.py
new file mode 100644
index 00000000..18995451
--- /dev/null
+++ b/narwhals/_exceptions.py
@@ -0,0 +1,4 @@
+from __future__ import annotations
+
+
+class ColumnNotFoundError(Exception): ...
diff --git a/narwhals/_expression_parsing.py b/narwhals/_expression_parsing.py
index dfc338dc..4c642239 100644
--- a/narwhals/_expression_parsing.py
+++ b/narwhals/_expression_parsing.py
@@ -11,7 +11,7 @@ from typing import Union
 from typing import cast
 from typing import overload
 
-from narwhals.dependencies import get_numpy
+from narwhals.dependencies import is_numpy_array
 from narwhals.utils import flatten
 
 if TYPE_CHECKING:
@@ -170,7 +170,7 @@ def parse_into_expr(
         return namespace._create_expr_from_series(into_expr)  # type: ignore[arg-type]
     if isinstance(into_expr, str):
         return namespace.col(into_expr)
-    if (np := get_numpy()) is not None and isinstance(into_expr, np.ndarray):
+    if is_numpy_array(into_expr):
         series = namespace._create_compliant_series(into_expr)
         return namespace._create_expr_from_series(series)  # type: ignore[arg-type]
     msg = f"Expected IntoExpr, got {type(into_expr)}"  # pragma: no cover
diff --git a/narwhals/_interchange/dataframe.py b/narwhals/_interchange/dataframe.py
index c7fc5ea3..bf1b1724 100644
--- a/narwhals/_interchange/dataframe.py
+++ b/narwhals/_interchange/dataframe.py
@@ -69,7 +69,7 @@ def map_interchange_dtype_to_narwhals_dtype(
 
 class InterchangeFrame:
     def __init__(self, df: Any) -> None:
-        self._native_dataframe = df
+        self._native_frame = df
 
     def __narwhals_dataframe__(self) -> Any:
         return self
@@ -77,15 +77,15 @@ class InterchangeFrame:
     def __getitem__(self, item: str) -> InterchangeSeries:
         from narwhals._interchange.series import InterchangeSeries
 
-        return InterchangeSeries(self._native_dataframe.get_column_by_name(item))
+        return InterchangeSeries(self._native_frame.get_column_by_name(item))
 
     @property
     def schema(self) -> dict[str, dtypes.DType]:
         return {
             column_name: map_interchange_dtype_to_narwhals_dtype(
-                self._native_dataframe.get_column_by_name(column_name).dtype
+                self._native_frame.get_column_by_name(column_name).dtype
             )
-            for column_name in self._native_dataframe.column_names()
+            for column_name in self._native_frame.column_names()
         }
 
     def __getattr__(self, attr: str) -> NoReturn:
diff --git a/narwhals/_pandas_like/dataframe.py b/narwhals/_pandas_like/dataframe.py
index 1f2a4500..c815b2a0 100644
--- a/narwhals/_pandas_like/dataframe.py
+++ b/narwhals/_pandas_like/dataframe.py
@@ -18,12 +18,12 @@ from narwhals._pandas_like.utils import translate_dtype
 from narwhals._pandas_like.utils import validate_dataframe_comparand
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
-from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
-from narwhals.dependencies import get_pyarrow
+from narwhals.dependencies import is_numpy_array
 from narwhals.utils import Implementation
 from narwhals.utils import flatten
 from narwhals.utils import generate_unique_token
+from narwhals.utils import parse_columns_to_drop
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -45,7 +45,7 @@ class PandasLikeDataFrame:
         backend_version: tuple[int, ...],
     ) -> None:
         self._validate_columns(native_dataframe.columns)
-        self._native_dataframe = native_dataframe
+        self._native_frame = native_dataframe
         self._implementation = implementation
         self._backend_version = backend_version
 
@@ -71,7 +71,7 @@ class PandasLikeDataFrame:
         raise AssertionError(msg)
 
     def __len__(self) -> int:
-        return len(self._native_dataframe)
+        return len(self._native_frame)
 
     def _validate_columns(self, columns: Sequence[str]) -> None:
         if len(columns) != len(set(columns)):
@@ -83,7 +83,7 @@ class PandasLikeDataFrame:
             msg = "Please report a bug"  # pragma: no cover
             raise AssertionError(msg)
 
-    def _from_native_dataframe(self, df: Any) -> Self:
+    def _from_native_frame(self, df: Any) -> Self:
         return self.__class__(
             df,
             implementation=self._implementation,
@@ -94,7 +94,7 @@ class PandasLikeDataFrame:
         from narwhals._pandas_like.series import PandasLikeSeries
 
         return PandasLikeSeries(
-            self._native_dataframe.loc[:, name],
+            self._native_frame.loc[:, name],
             implementation=self._implementation,
             backend_version=self._backend_version,
         )
@@ -118,7 +118,7 @@ class PandasLikeDataFrame:
             from narwhals._pandas_like.series import PandasLikeSeries
 
             return PandasLikeSeries(
-                self._native_dataframe.loc[:, item],
+                self._native_frame.loc[:, item],
                 implementation=self._implementation,
                 backend_version=self._backend_version,
             )
@@ -129,13 +129,13 @@ class PandasLikeDataFrame:
             and isinstance(item[1], (tuple, list))
         ):
             if all(isinstance(x, int) for x in item[1]):
-                return self._from_native_dataframe(self._native_dataframe.iloc[item])
+                return self._from_native_frame(self._native_frame.iloc[item])
             if all(isinstance(x, str) for x in item[1]):
                 item = (
                     item[0],
-                    self._native_dataframe.columns.get_indexer(item[1]),
+                    self._native_frame.columns.get_indexer(item[1]),
                 )
-                return self._from_native_dataframe(self._native_dataframe.iloc[item])
+                return self._from_native_frame(self._native_frame.iloc[item])
             msg = (
                 f"Expected sequence str or int, got: {type(item[1])}"  # pragma: no cover
             )
@@ -145,10 +145,10 @@ class PandasLikeDataFrame:
             from narwhals._pandas_like.series import PandasLikeSeries
 
             if isinstance(item[1], str):
-                item = (item[0], self._native_dataframe.columns.get_loc(item[1]))
-                native_series = self._native_dataframe.iloc[item]
+                item = (item[0], self._native_frame.columns.get_loc(item[1]))
+                native_series = self._native_frame.iloc[item]
             elif isinstance(item[1], int):
-                native_series = self._native_dataframe.iloc[item]
+                native_series = self._native_frame.iloc[item]
             else:  # pragma: no cover
                 msg = f"Expected str or int, got: {type(item[1])}"
                 raise TypeError(msg)
@@ -160,11 +160,9 @@ class PandasLikeDataFrame:
             )
 
         elif isinstance(item, (slice, Sequence)) or (
-            (np := get_numpy()) is not None
-            and isinstance(item, np.ndarray)
-            and item.ndim == 1
+            is_numpy_array(item) and item.ndim == 1
         ):
-            return self._from_native_dataframe(self._native_dataframe.iloc[item])
+            return self._from_native_frame(self._native_frame.iloc[item])
 
         else:  # pragma: no cover
             msg = f"Expected str or slice, got: {type(item)}"
@@ -173,15 +171,15 @@ class PandasLikeDataFrame:
     # --- properties ---
     @property
     def columns(self) -> list[str]:
-        return self._native_dataframe.columns.tolist()  # type: ignore[no-any-return]
+        return self._native_frame.columns.tolist()  # type: ignore[no-any-return]
 
     def rows(
         self, *, named: bool = False
     ) -> list[tuple[Any, ...]] | list[dict[str, Any]]:
         if not named:
-            return list(self._native_dataframe.itertuples(index=False, name=None))
+            return list(self._native_frame.itertuples(index=False, name=None))
 
-        return self._native_dataframe.to_dict(orient="records")  # type: ignore[no-any-return]
+        return self._native_frame.to_dict(orient="records")  # type: ignore[no-any-return]
 
     def iter_rows(
         self,
@@ -195,19 +193,19 @@ class PandasLikeDataFrame:
             and has no effect on the output.
         """
         if not named:
-            yield from self._native_dataframe.itertuples(index=False, name=None)
+            yield from self._native_frame.itertuples(index=False, name=None)
         else:
-            col_names = self._native_dataframe.columns
+            col_names = self._native_frame.columns
             yield from (
                 dict(zip(col_names, row))
-                for row in self._native_dataframe.itertuples(index=False)
+                for row in self._native_frame.itertuples(index=False)
             )  # type: ignore[misc]
 
     @property
     def schema(self) -> dict[str, DType]:
         return {
-            col: translate_dtype(self._native_dataframe.loc[:, col])
-            for col in self._native_dataframe.columns
+            col: translate_dtype(self._native_frame.loc[:, col])
+            for col in self._native_frame.columns
         }
 
     def collect_schema(self) -> dict[str, DType]:
@@ -221,36 +219,36 @@ class PandasLikeDataFrame:
     ) -> Self:
         if exprs and all(isinstance(x, str) for x in exprs) and not named_exprs:
             # This is a simple slice => fastpath!
-            return self._from_native_dataframe(self._native_dataframe.loc[:, exprs])
+            return self._from_native_frame(self._native_frame.loc[:, exprs])
         new_series = evaluate_into_exprs(self, *exprs, **named_exprs)
         if not new_series:
             # return empty dataframe, like Polars does
-            return self._from_native_dataframe(self._native_dataframe.__class__())
+            return self._from_native_frame(self._native_frame.__class__())
         new_series = broadcast_series(new_series)
         df = horizontal_concat(
             new_series,
             implementation=self._implementation,
             backend_version=self._backend_version,
         )
-        return self._from_native_dataframe(df)
+        return self._from_native_frame(df)
 
     def drop_nulls(self, subset: str | list[str] | None) -> Self:
         if subset is None:
-            return self._from_native_dataframe(self._native_dataframe.dropna(axis=0))
+            return self._from_native_frame(self._native_frame.dropna(axis=0))
         subset = [subset] if isinstance(subset, str) else subset
         plx = self.__narwhals_namespace__()
         return self.filter(~plx.any_horizontal(plx.col(*subset).is_null()))
 
     def with_row_index(self, name: str) -> Self:
         row_index = create_native_series(
-            range(len(self._native_dataframe)),
-            index=self._native_dataframe.index,
+            range(len(self._native_frame)),
+            index=self._native_frame.index,
             implementation=self._implementation,
             backend_version=self._backend_version,
         ).alias(name)
-        return self._from_native_dataframe(
+        return self._from_native_frame(
             horizontal_concat(
-                [row_index._native_series, self._native_dataframe],
+                [row_index._native_series, self._native_frame],
                 implementation=self._implementation,
                 backend_version=self._backend_version,
             )
@@ -260,21 +258,19 @@ class PandasLikeDataFrame:
         self,
         *predicates: IntoPandasLikeExpr,
     ) -> Self:
-        from narwhals._pandas_like.namespace import PandasLikeNamespace
-
-        plx = PandasLikeNamespace(self._implementation, self._backend_version)
+        plx = self.__narwhals_namespace__()
         expr = plx.all_horizontal(*predicates)
         # Safety: all_horizontal's expression only returns a single column.
         mask = expr._call(self)[0]
-        _mask = validate_dataframe_comparand(self._native_dataframe.index, mask)
-        return self._from_native_dataframe(self._native_dataframe.loc[_mask])
+        _mask = validate_dataframe_comparand(self._native_frame.index, mask)
+        return self._from_native_frame(self._native_frame.loc[_mask])
 
     def with_columns(
         self,
         *exprs: IntoPandasLikeExpr,
         **named_exprs: IntoPandasLikeExpr,
     ) -> Self:
-        index = self._native_dataframe.index
+        index = self._native_frame.index
         new_columns = evaluate_into_exprs(self, *exprs, **named_exprs)
 
         if not new_columns and len(self) == 0:
@@ -295,7 +291,7 @@ class PandasLikeDataFrame:
             new_column_name_to_new_column_map = {s.name: s for s in new_columns}
             to_concat = []
             # Make sure to preserve column order
-            for name in self._native_dataframe.columns:
+            for name in self._native_frame.columns:
                 if name in new_column_name_to_new_column_map:
                     to_concat.append(
                         validate_dataframe_comparand(
@@ -303,7 +299,7 @@ class PandasLikeDataFrame:
                         )
                     )
                 else:
-                    to_concat.append(self._native_dataframe.loc[:, name])
+                    to_concat.append(self._native_frame.loc[:, name])
             to_concat.extend(
                 validate_dataframe_comparand(index, new_column_name_to_new_column_map[s])
                 for s in new_column_name_to_new_column_map
@@ -315,18 +311,19 @@ class PandasLikeDataFrame:
                 backend_version=self._backend_version,
             )
         else:
-            df = self._native_dataframe.copy(deep=False)
+            df = self._native_frame.copy(deep=False)
             for s in new_columns:
                 df[s.name] = validate_dataframe_comparand(index, s)
-        return self._from_native_dataframe(df)
+        return self._from_native_frame(df)
 
     def rename(self, mapping: dict[str, str]) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.rename(columns=mapping))
+        return self._from_native_frame(self._native_frame.rename(columns=mapping))
 
-    def drop(self, *columns: str) -> Self:
-        return self._from_native_dataframe(
-            self._native_dataframe.drop(columns=list(columns))
+    def drop(self: Self, columns: list[str], strict: bool) -> Self:  # noqa: FBT001
+        to_drop = parse_columns_to_drop(
+            compliant_frame=self, columns=columns, strict=strict
         )
+        return self._from_native_frame(self._native_frame.drop(columns=to_drop))
 
     # --- transform ---
     def sort(
@@ -336,17 +333,17 @@ class PandasLikeDataFrame:
         descending: bool | Sequence[bool] = False,
     ) -> Self:
         flat_keys = flatten([*flatten([by]), *more_by])
-        df = self._native_dataframe
+        df = self._native_frame
         if isinstance(descending, bool):
             ascending: bool | list[bool] = not descending
         else:
             ascending = [not d for d in descending]
-        return self._from_native_dataframe(df.sort_values(flat_keys, ascending=ascending))
+        return self._from_native_frame(df.sort_values(flat_keys, ascending=ascending))
 
     # --- convert ---
     def collect(self) -> PandasLikeDataFrame:
         return PandasLikeDataFrame(
-            self._native_dataframe,
+            self._native_frame,
             implementation=self._implementation,
             backend_version=self._backend_version,
         )
@@ -385,19 +382,21 @@ class PandasLikeDataFrame:
                     n_bytes=8, columns=[*self.columns, *other.columns]
                 )
 
-                return self._from_native_dataframe(
-                    self._native_dataframe.assign(**{key_token: 0}).merge(
-                        other._native_dataframe.assign(**{key_token: 0}),
+                return self._from_native_frame(
+                    self._native_frame.assign(**{key_token: 0})
+                    .merge(
+                        other._native_frame.assign(**{key_token: 0}),
                         how="inner",
                         left_on=key_token,
                         right_on=key_token,
                         suffixes=("", "_right"),
-                    ),
-                ).drop(key_token)
+                    )
+                    .drop(columns=key_token),
+                )
             else:
-                return self._from_native_dataframe(
-                    self._native_dataframe.merge(
-                        other._native_dataframe,
+                return self._from_native_frame(
+                    self._native_frame.merge(
+                        other._native_frame,
                         how="cross",
                         suffixes=("", "_right"),
                     ),
@@ -409,14 +408,14 @@ class PandasLikeDataFrame:
             )
 
             other_native = (
-                other._native_dataframe.loc[:, right_on]
+                other._native_frame.loc[:, right_on]
                 .rename(  # rename to avoid creating extra columns in join
                     columns=dict(zip(right_on, left_on))  # type: ignore[arg-type]
                 )
                 .drop_duplicates()
             )
-            return self._from_native_dataframe(
-                self._native_dataframe.merge(
+            return self._from_native_frame(
+                self._native_frame.merge(
                     other_native,
                     how="outer",
                     indicator=indicator_token,
@@ -424,19 +423,19 @@ class PandasLikeDataFrame:
                     right_on=left_on,
                 )
                 .loc[lambda t: t[indicator_token] == "left_only"]
-                .drop(columns=[indicator_token])
+                .drop(columns=indicator_token)
             )
 
         if how == "semi":
             other_native = (
-                other._native_dataframe.loc[:, right_on]
+                other._native_frame.loc[:, right_on]
                 .rename(  # rename to avoid creating extra columns in join
                     columns=dict(zip(right_on, left_on))  # type: ignore[arg-type]
                 )
                 .drop_duplicates()  # avoids potential rows duplication from inner join
             )
-            return self._from_native_dataframe(
-                self._native_dataframe.merge(
+            return self._from_native_frame(
+                self._native_frame.merge(
                     other_native,
                     how="inner",
                     left_on=left_on,
@@ -445,8 +444,8 @@ class PandasLikeDataFrame:
             )
 
         if how == "left":
-            other_native = other._native_dataframe
-            result_native = self._native_dataframe.merge(
+            other_native = other._native_frame
+            result_native = self._native_frame.merge(
                 other_native,
                 how="left",
                 left_on=left_on,
@@ -459,11 +458,11 @@ class PandasLikeDataFrame:
                     extra.append(right_key)
                 elif right_key != left_key:
                     extra.append(f"{right_key}_right")
-            return self._from_native_dataframe(result_native.drop(columns=extra))
+            return self._from_native_frame(result_native.drop(columns=extra))
 
-        return self._from_native_dataframe(
-            self._native_dataframe.merge(
-                other._native_dataframe,
+        return self._from_native_frame(
+            self._native_frame.merge(
+                other._native_frame,
                 left_on=left_on,
                 right_on=right_on,
                 how=how,
@@ -474,10 +473,10 @@ class PandasLikeDataFrame:
     # --- partial reduction ---
 
     def head(self, n: int) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.head(n))
+        return self._from_native_frame(self._native_frame.head(n))
 
     def tail(self, n: int) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.tail(n))
+        return self._from_native_frame(self._native_frame.tail(n))
 
     def unique(
         self: Self,
@@ -491,11 +490,10 @@ class PandasLikeDataFrame:
             The param `maintain_order` is only here for compatibility with the polars API
             and has no effect on the output.
         """
-
         mapped_keep = {"none": False, "any": "first"}.get(keep, keep)
         subset = flatten(subset) if subset else None
-        return self._from_native_dataframe(
-            self._native_dataframe.drop_duplicates(subset=subset, keep=mapped_keep)
+        return self._from_native_frame(
+            self._native_frame.drop_duplicates(subset=subset, keep=mapped_keep)
         )
 
     # --- lazy-only ---
@@ -504,7 +502,7 @@ class PandasLikeDataFrame:
 
     @property
     def shape(self) -> tuple[int, int]:
-        return self._native_dataframe.shape  # type: ignore[no-any-return]
+        return self._native_frame.shape  # type: ignore[no-any-return]
 
     def to_dict(self, *, as_series: bool = False) -> dict[str, Any]:
         from narwhals._pandas_like.series import PandasLikeSeries
@@ -513,13 +511,13 @@ class PandasLikeDataFrame:
             # TODO(Unassigned): should this return narwhals series?
             return {
                 col: PandasLikeSeries(
-                    self._native_dataframe.loc[:, col],
+                    self._native_frame.loc[:, col],
                     implementation=self._implementation,
                     backend_version=self._backend_version,
                 )
                 for col in self.columns
             }
-        return self._native_dataframe.to_dict(orient="list")  # type: ignore[no-any-return]
+        return self._native_frame.to_dict(orient="list")  # type: ignore[no-any-return]
 
     def to_numpy(self) -> Any:
         from narwhals._pandas_like.series import PANDAS_TO_NUMPY_DTYPE_MISSING
@@ -528,48 +526,48 @@ class PandasLikeDataFrame:
         # Series to numpy and let numpy find a common dtype.
         # If there aren't any dtypes where `to_numpy()` is "broken" (i.e. it
         # returns Object) then we just call `to_numpy()` on the DataFrame.
-        for dtype in self._native_dataframe.dtypes:
+        for dtype in self._native_frame.dtypes:
             if str(dtype) in PANDAS_TO_NUMPY_DTYPE_MISSING:
-                import numpy as np
+                import numpy as np  # ignore-banned-import
 
                 return np.hstack([self[col].to_numpy()[:, None] for col in self.columns])
-        return self._native_dataframe.to_numpy()
+        return self._native_frame.to_numpy()
 
     def to_pandas(self) -> Any:
         if self._implementation is Implementation.PANDAS:
-            return self._native_dataframe
+            return self._native_frame
         if self._implementation is Implementation.MODIN:  # pragma: no cover
-            return self._native_dataframe._to_pandas()
-        return self._native_dataframe.to_pandas()  # pragma: no cover
+            return self._native_frame._to_pandas()
+        return self._native_frame.to_pandas()  # pragma: no cover
 
     def write_parquet(self, file: Any) -> Any:
-        self._native_dataframe.to_parquet(file)
+        self._native_frame.to_parquet(file)
 
     # --- descriptive ---
     def is_duplicated(self: Self) -> PandasLikeSeries:
         from narwhals._pandas_like.series import PandasLikeSeries
 
         return PandasLikeSeries(
-            self._native_dataframe.duplicated(keep=False),
+            self._native_frame.duplicated(keep=False),
             implementation=self._implementation,
             backend_version=self._backend_version,
         )
 
     def is_empty(self: Self) -> bool:
-        return self._native_dataframe.empty  # type: ignore[no-any-return]
+        return self._native_frame.empty  # type: ignore[no-any-return]
 
     def is_unique(self: Self) -> PandasLikeSeries:
         from narwhals._pandas_like.series import PandasLikeSeries
 
         return PandasLikeSeries(
-            ~self._native_dataframe.duplicated(keep=False),
+            ~self._native_frame.duplicated(keep=False),
             implementation=self._implementation,
             backend_version=self._backend_version,
         )
 
     def null_count(self: Self) -> PandasLikeDataFrame:
         return PandasLikeDataFrame(
-            self._native_dataframe.isna().sum(axis=0).to_frame().transpose(),
+            self._native_frame.isna().sum(axis=0).to_frame().transpose(),
             implementation=self._implementation,
             backend_version=self._backend_version,
         )
@@ -583,25 +581,26 @@ class PandasLikeDataFrame:
                     f" frame has shape {self.shape!r}"
                 )
                 raise ValueError(msg)
-            return self._native_dataframe.iloc[0, 0]
+            return self._native_frame.iloc[0, 0]
 
         elif row is None or column is None:
             msg = "cannot call `.item()` with only one of `row` or `column`"
             raise ValueError(msg)
 
         _col = self.columns.index(column) if isinstance(column, str) else column
-        return self._native_dataframe.iloc[row, _col]
+        return self._native_frame.iloc[row, _col]
 
     def clone(self: Self) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.copy())
+        return self._from_native_frame(self._native_frame.copy())
 
     def gather_every(self: Self, n: int, offset: int = 0) -> Self:
-        return self._from_native_dataframe(self._native_dataframe.iloc[offset::n])
+        return self._from_native_frame(self._native_frame.iloc[offset::n])
 
     def to_arrow(self: Self) -> Any:
         if self._implementation is Implementation.CUDF:  # pragma: no cover
             msg = "`to_arrow` is not implemented for CuDF backend."
             raise NotImplementedError(msg)
 
-        pa = get_pyarrow()
-        return pa.Table.from_pandas(self._native_dataframe)
+        import pyarrow as pa  # ignore-banned-import()
+
+        return pa.Table.from_pandas(self._native_frame)
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 1f62dc64..193b1786 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -64,7 +64,7 @@ class PandasLikeExpr:
         def func(df: PandasLikeDataFrame) -> list[PandasLikeSeries]:
             return [
                 PandasLikeSeries(
-                    df._native_dataframe.loc[:, column_name],
+                    df._native_frame.loc[:, column_name],
                     implementation=df._implementation,
                     backend_version=df._backend_version,
                 )
@@ -226,9 +226,7 @@ class PandasLikeExpr:
         return reuse_series_implementation(self, "arg_true")
 
     def filter(self, *predicates: Any) -> Self:
-        from narwhals._pandas_like.namespace import PandasLikeNamespace
-
-        plx = PandasLikeNamespace(self._implementation, self._backend_version)
+        plx = self.__narwhals_namespace__()
         expr = plx.all_horizontal(*predicates)
         return reuse_series_implementation(self, "filter", other=expr)
 
diff --git a/narwhals/_pandas_like/group_by.py b/narwhals/_pandas_like/group_by.py
index 129b047b..11abc85c 100644
--- a/narwhals/_pandas_like/group_by.py
+++ b/narwhals/_pandas_like/group_by.py
@@ -32,16 +32,16 @@ class PandasLikeGroupBy:
             self._df._implementation is Implementation.PANDAS
             and self._df._backend_version < (1, 0)
         ):  # pragma: no cover
-            if self._df._native_dataframe.loc[:, self._keys].isna().any().any():
+            if self._df._native_frame.loc[:, self._keys].isna().any().any():
                 msg = "Grouping by null values is not supported in pandas < 1.0.0"
                 raise NotImplementedError(msg)
-            self._grouped = self._df._native_dataframe.groupby(
+            self._grouped = self._df._native_frame.groupby(
                 list(self._keys),
                 sort=False,
                 as_index=True,
             )
         else:
-            self._grouped = self._df._native_dataframe.groupby(
+            self._grouped = self._df._native_frame.groupby(
                 list(self._keys),
                 sort=False,
                 as_index=True,
@@ -75,13 +75,13 @@ class PandasLikeGroupBy:
             exprs,
             self._keys,
             output_names,
-            self._from_native_dataframe,
-            dataframe_is_empty=self._df._native_dataframe.empty,
+            self._from_native_frame,
+            dataframe_is_empty=self._df._native_frame.empty,
             implementation=implementation,
             backend_version=self._df._backend_version,
         )
 
-    def _from_native_dataframe(self, df: PandasLikeDataFrame) -> PandasLikeDataFrame:
+    def _from_native_frame(self, df: PandasLikeDataFrame) -> PandasLikeDataFrame:
         from narwhals._pandas_like.dataframe import PandasLikeDataFrame
 
         return PandasLikeDataFrame(
@@ -100,9 +100,7 @@ class PandasLikeGroupBy:
                 category=FutureWarning,
             )
             iterator = self._grouped.__iter__()
-        yield from (
-            (key, self._from_native_dataframe(sub_df)) for (key, sub_df) in iterator
-        )
+        yield from ((key, self._from_native_frame(sub_df)) for (key, sub_df) in iterator)
 
 
 def agg_pandas(
diff --git a/narwhals/_pandas_like/namespace.py b/narwhals/_pandas_like/namespace.py
index 13e2e99d..1d07fde2 100644
--- a/narwhals/_pandas_like/namespace.py
+++ b/narwhals/_pandas_like/namespace.py
@@ -115,7 +115,7 @@ class PandasLikeNamespace:
         return PandasLikeExpr(
             lambda df: [
                 PandasLikeSeries(
-                    df._native_dataframe.loc[:, column_name],
+                    df._native_frame.loc[:, column_name],
                     implementation=self._implementation,
                     backend_version=self._backend_version,
                 )
@@ -134,7 +134,7 @@ class PandasLikeNamespace:
             pandas_series = PandasLikeSeries._from_iterable(
                 data=[value],
                 name="lit",
-                index=df._native_dataframe.index[0:1],
+                index=df._native_frame.index[0:1],
                 implementation=self._implementation,
                 backend_version=self._backend_version,
             )
@@ -185,7 +185,7 @@ class PandasLikeNamespace:
         return PandasLikeExpr(
             lambda df: [
                 PandasLikeSeries._from_iterable(
-                    [len(df._native_dataframe)],
+                    [len(df._native_frame)],
                     name="len",
                     index=[0],
                     implementation=self._implementation,
@@ -216,7 +216,7 @@ class PandasLikeNamespace:
         *,
         how: str = "vertical",
     ) -> PandasLikeDataFrame:
-        dfs: list[Any] = [item._native_dataframe for item in items]
+        dfs: list[Any] = [item._native_frame for item in items]
         if how == "horizontal":
             return PandasLikeDataFrame(
                 horizontal_concat(
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 81594c38..3db0fb73 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -8,24 +8,20 @@ from typing import Sequence
 from typing import overload
 
 from narwhals._pandas_like.utils import int_dtype_mapper
+from narwhals._pandas_like.utils import narwhals_to_native_dtype
 from narwhals._pandas_like.utils import native_series_from_iterable
-from narwhals._pandas_like.utils import reverse_translate_dtype
 from narwhals._pandas_like.utils import to_datetime
 from narwhals._pandas_like.utils import translate_dtype
 from narwhals._pandas_like.utils import validate_column_comparand
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_modin
-from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_pandas
-from narwhals.dependencies import get_pyarrow
-from narwhals.dependencies import get_pyarrow_compute
 from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
     from typing_extensions import Self
 
     from narwhals._pandas_like.dataframe import PandasLikeDataFrame
-    from narwhals._pandas_like.namespace import PandasLikeNamespace
     from narwhals.dtypes import DType
 
 PANDAS_TO_NUMPY_DTYPE_NO_MISSING = {
@@ -99,11 +95,6 @@ class PandasLikeSeries:
         else:
             self._use_copy_false = False
 
-    def __narwhals_namespace__(self) -> PandasLikeNamespace:
-        from narwhals._pandas_like.namespace import PandasLikeNamespace
-
-        return PandasLikeNamespace(self._implementation, self._backend_version)
-
     def __native_namespace__(self) -> Any:
         if self._implementation is Implementation.PANDAS:
             return get_pandas()
@@ -181,7 +172,7 @@ class PandasLikeSeries:
         dtype: Any,
     ) -> Self:
         ser = self._native_series
-        dtype = reverse_translate_dtype(dtype, ser.dtype, self._implementation)
+        dtype = narwhals_to_native_dtype(dtype, ser.dtype, self._implementation)
         return self._from_native_series(ser.astype(dtype))
 
     def item(self: Self, index: int | None = None) -> Any:
@@ -230,7 +221,8 @@ class PandasLikeSeries:
         return self._from_native_series(res)
 
     def arg_true(self) -> PandasLikeSeries:
-        np = get_numpy()
+        import numpy as np  # ignore-banned-import
+
         ser = self._native_series
         res = np.flatnonzero(ser)
         return self._from_native_series(
@@ -644,7 +636,8 @@ class PandasLikeSeries:
             msg = "`to_arrow` is not implemented for CuDF backend."
             raise NotImplementedError(msg)
 
-        pa = get_pyarrow()
+        import pyarrow as pa  # ignore-banned-import()
+
         return pa.Array.from_pandas(self._native_series)
 
     @property
@@ -792,7 +785,8 @@ class PandasLikeSeriesDateTimeNamespace:
             self._pandas_series._native_series.dtype
         ):
             # crazy workaround for https://github.com/pandas-dev/pandas/issues/59154
-            pc = get_pyarrow_compute()
+            import pyarrow.compute as pc  # ignore-banned-import()
+
             native_series = self._pandas_series._native_series
             arr = native_series.array.__arrow_array__()
             result_arr = pc.add(
diff --git a/narwhals/_pandas_like/utils.py b/narwhals/_pandas_like/utils.py
index 71ebbdbc..e8065408 100644
--- a/narwhals/_pandas_like/utils.py
+++ b/narwhals/_pandas_like/utils.py
@@ -303,11 +303,20 @@ def get_dtype_backend(dtype: Any, implementation: Implementation) -> str:
         return "numpy"
 
 
-def reverse_translate_dtype(  # noqa: PLR0915
+def narwhals_to_native_dtype(  # noqa: PLR0915
     dtype: DType | type[DType], starting_dtype: Any, implementation: Implementation
 ) -> Any:
     from narwhals import dtypes
 
+    if "polars" in str(type(dtype)):
+        msg = (
+            f"Expected Narwhals object, got: {type(dtype)}.\n\n"
+            "Perhaps you:\n"
+            "- Forgot a `nw.from_native` somewhere?\n"
+            "- Used `pl.Int64` instead of `nw.Int64`?"
+        )
+        raise TypeError(msg)
+
     dtype_backend = get_dtype_backend(starting_dtype, implementation)
     if isinstance_or_issubclass(dtype, dtypes.Float64):
         if dtype_backend == "pyarrow-nullable":
@@ -413,6 +422,9 @@ def reverse_translate_dtype(  # noqa: PLR0915
             return "date32[pyarrow]"
         msg = "Date dtype only supported for pyarrow-backed data types in pandas"
         raise NotImplementedError(msg)
+    if isinstance_or_issubclass(dtype, dtypes.Enum):
+        msg = "Converting to Enum is not (yet) supported"
+        raise NotImplementedError(msg)
     msg = f"Unknown dtype: {dtype}"  # pragma: no cover
     raise AssertionError(msg)
 
diff --git a/narwhals/_polars/dataframe.py b/narwhals/_polars/dataframe.py
index f137739f..2137f891 100644
--- a/narwhals/_polars/dataframe.py
+++ b/narwhals/_polars/dataframe.py
@@ -8,6 +8,7 @@ from narwhals._polars.utils import extract_args_kwargs
 from narwhals._polars.utils import translate_dtype
 from narwhals.dependencies import get_polars
 from narwhals.utils import Implementation
+from narwhals.utils import parse_columns_to_drop
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -15,7 +16,7 @@ if TYPE_CHECKING:
 
 class PolarsDataFrame:
     def __init__(self, df: Any, *, backend_version: tuple[int, ...]) -> None:
-        self._native_dataframe = df
+        self._native_frame = df
         self._implementation = Implementation.POLARS
         self._backend_version = backend_version
 
@@ -31,7 +32,7 @@ class PolarsDataFrame:
     def __native_namespace__(self) -> Any:
         return get_polars()
 
-    def _from_native_dataframe(self, df: Any) -> Self:
+    def _from_native_frame(self, df: Any) -> Self:
         return self.__class__(df, backend_version=self._backend_version)
 
     def _from_native_object(self, obj: Any) -> Any:
@@ -41,7 +42,7 @@ class PolarsDataFrame:
 
             return PolarsSeries(obj, backend_version=self._backend_version)
         if isinstance(obj, pl.DataFrame):
-            return self._from_native_dataframe(obj)
+            return self._from_native_frame(obj)
         # scalar
         return obj
 
@@ -52,30 +53,30 @@ class PolarsDataFrame:
         def func(*args: Any, **kwargs: Any) -> Any:
             args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
             return self._from_native_object(
-                getattr(self._native_dataframe, attr)(*args, **kwargs)
+                getattr(self._native_frame, attr)(*args, **kwargs)
             )
 
         return func
 
     @property
     def schema(self) -> dict[str, Any]:
-        schema = self._native_dataframe.schema
+        schema = self._native_frame.schema
         return {name: translate_dtype(dtype) for name, dtype in schema.items()}
 
     def collect_schema(self) -> dict[str, Any]:
         if self._backend_version < (1,):  # pragma: no cover
-            schema = self._native_dataframe.schema
+            schema = self._native_frame.schema
         else:
-            schema = dict(self._native_dataframe.collect_schema())
+            schema = dict(self._native_frame.collect_schema())
         return {name: translate_dtype(dtype) for name, dtype in schema.items()}
 
     @property
     def shape(self) -> tuple[int, int]:
-        return self._native_dataframe.shape  # type: ignore[no-any-return]
+        return self._native_frame.shape  # type: ignore[no-any-return]
 
     def __getitem__(self, item: Any) -> Any:
         pl = get_polars()
-        result = self._native_dataframe.__getitem__(item)
+        result = self._native_frame.__getitem__(item)
         if isinstance(result, pl.Series):
             from narwhals._polars.series import PolarsSeries
 
@@ -86,23 +87,23 @@ class PolarsDataFrame:
         from narwhals._polars.series import PolarsSeries
 
         return PolarsSeries(
-            self._native_dataframe.get_column(name), backend_version=self._backend_version
+            self._native_frame.get_column(name), backend_version=self._backend_version
         )
 
     def is_empty(self) -> bool:
-        return len(self._native_dataframe) == 0
+        return len(self._native_frame) == 0
 
     @property
     def columns(self) -> list[str]:
-        return self._native_dataframe.columns  # type: ignore[no-any-return]
+        return self._native_frame.columns  # type: ignore[no-any-return]
 
     def lazy(self) -> PolarsLazyFrame:
         return PolarsLazyFrame(
-            self._native_dataframe.lazy(), backend_version=self._backend_version
+            self._native_frame.lazy(), backend_version=self._backend_version
         )
 
     def to_dict(self, *, as_series: bool) -> Any:
-        df = self._native_dataframe
+        df = self._native_frame
 
         if as_series:
             from narwhals._polars.series import PolarsSeries
@@ -121,15 +122,21 @@ class PolarsDataFrame:
 
     def with_row_index(self, name: str) -> Any:
         if self._backend_version < (0, 20, 4):  # pragma: no cover
-            return self._from_native_dataframe(
-                self._native_dataframe.with_row_count(name)
+            return self._from_native_frame(self._native_frame.with_row_count(name))
+        return self._from_native_frame(self._native_frame.with_row_index(name))
+
+    def drop(self: Self, columns: list[str], strict: bool) -> Self:  # noqa: FBT001
+        if self._backend_version < (1, 0, 0):  # pragma: no cover
+            to_drop = parse_columns_to_drop(
+                compliant_frame=self, columns=columns, strict=strict
             )
-        return self._from_native_dataframe(self._native_dataframe.with_row_index(name))
+            return self._from_native_frame(self._native_frame.drop(to_drop))
+        return self._from_native_frame(self._native_frame.drop(columns, strict=strict))
 
 
 class PolarsLazyFrame:
     def __init__(self, df: Any, *, backend_version: tuple[int, ...]) -> None:
-        self._native_dataframe = df
+        self._native_frame = df
         self._backend_version = backend_version
 
     def __repr__(self) -> str:  # pragma: no cover
@@ -144,37 +151,37 @@ class PolarsLazyFrame:
     def __native_namespace__(self) -> Any:  # pragma: no cover
         return get_polars()
 
-    def _from_native_dataframe(self, df: Any) -> Self:
+    def _from_native_frame(self, df: Any) -> Self:
         return self.__class__(df, backend_version=self._backend_version)
 
     def __getattr__(self, attr: str) -> Any:
         def func(*args: Any, **kwargs: Any) -> Any:
             args, kwargs = extract_args_kwargs(args, kwargs)  # type: ignore[assignment]
-            return self._from_native_dataframe(
-                getattr(self._native_dataframe, attr)(*args, **kwargs)
+            return self._from_native_frame(
+                getattr(self._native_frame, attr)(*args, **kwargs)
             )
 
         return func
 
     @property
     def columns(self) -> list[str]:
-        return self._native_dataframe.columns  # type: ignore[no-any-return]
+        return self._native_frame.columns  # type: ignore[no-any-return]
 
     @property
     def schema(self) -> dict[str, Any]:
-        schema = self._native_dataframe.schema
+        schema = self._native_frame.schema
         return {name: translate_dtype(dtype) for name, dtype in schema.items()}
 
     def collect_schema(self) -> dict[str, Any]:
         if self._backend_version < (1,):  # pragma: no cover
-            schema = self._native_dataframe.schema
+            schema = self._native_frame.schema
         else:
-            schema = dict(self._native_dataframe.collect_schema())
+            schema = dict(self._native_frame.collect_schema())
         return {name: translate_dtype(dtype) for name, dtype in schema.items()}
 
     def collect(self) -> PolarsDataFrame:
         return PolarsDataFrame(
-            self._native_dataframe.collect(), backend_version=self._backend_version
+            self._native_frame.collect(), backend_version=self._backend_version
         )
 
     def group_by(self, *by: str) -> Any:
@@ -184,7 +191,10 @@ class PolarsLazyFrame:
 
     def with_row_index(self, name: str) -> Any:
         if self._backend_version < (0, 20, 4):  # pragma: no cover
-            return self._from_native_dataframe(
-                self._native_dataframe.with_row_count(name)
-            )
-        return self._from_native_dataframe(self._native_dataframe.with_row_index(name))
+            return self._from_native_frame(self._native_frame.with_row_count(name))
+        return self._from_native_frame(self._native_frame.with_row_index(name))
+
+    def drop(self: Self, columns: list[str], strict: bool) -> Self:  # noqa: FBT001
+        if self._backend_version < (1, 0, 0):  # pragma: no cover
+            return self._from_native_frame(self._native_frame.drop(columns))
+        return self._from_native_frame(self._native_frame.drop(columns, strict=strict))
diff --git a/narwhals/_polars/expr.py b/narwhals/_polars/expr.py
index 98aac298..4f153282 100644
--- a/narwhals/_polars/expr.py
+++ b/narwhals/_polars/expr.py
@@ -3,10 +3,9 @@ from __future__ import annotations
 from typing import TYPE_CHECKING
 from typing import Any
 
-from narwhals._polars.namespace import PolarsNamespace
 from narwhals._polars.utils import extract_args_kwargs
 from narwhals._polars.utils import extract_native
-from narwhals._polars.utils import reverse_translate_dtype
+from narwhals._polars.utils import narwhals_to_native_dtype
 from narwhals.utils import Implementation
 
 if TYPE_CHECKING:
@@ -23,12 +22,6 @@ class PolarsExpr:
     def __repr__(self) -> str:  # pragma: no cover
         return "PolarsExpr"
 
-    def __narwhals_expr__(self) -> Self:  # pragma: no cover
-        return self
-
-    def __narwhals_namespace__(self) -> PolarsNamespace:  # pragma: no cover
-        return PolarsNamespace(backend_version=self._backend_version)
-
     def _from_native_expr(self, expr: Any) -> Self:
         return self.__class__(expr)
 
@@ -43,7 +36,7 @@ class PolarsExpr:
 
     def cast(self, dtype: DType) -> Self:
         expr = self._native_expr
-        dtype = reverse_translate_dtype(dtype)
+        dtype = narwhals_to_native_dtype(dtype)
         return self._from_native_expr(expr.cast(dtype))
 
     def __eq__(self, other: object) -> Self:  # type: ignore[override]
diff --git a/narwhals/_polars/group_by.py b/narwhals/_polars/group_by.py
index c0de7573..f03da610 100644
--- a/narwhals/_polars/group_by.py
+++ b/narwhals/_polars/group_by.py
@@ -14,27 +14,27 @@ class PolarsGroupBy:
     def __init__(self, df: Any, keys: list[str]) -> None:
         self._compliant_frame = df
         self.keys = keys
-        self._grouped = df._native_dataframe.group_by(keys)
+        self._grouped = df._native_frame.group_by(keys)
 
     def agg(self, *aggs: Any, **named_aggs: Any) -> PolarsDataFrame:
         aggs, named_aggs = extract_args_kwargs(aggs, named_aggs)  # type: ignore[assignment]
-        return self._compliant_frame._from_native_dataframe(  # type: ignore[no-any-return]
+        return self._compliant_frame._from_native_frame(  # type: ignore[no-any-return]
             self._grouped.agg(*aggs, **named_aggs),
         )
 
     def __iter__(self) -> Any:
         for key, df in self._grouped:
-            yield tuple(key), self._compliant_frame._from_native_dataframe(df)
+            yield tuple(key), self._compliant_frame._from_native_frame(df)
 
 
 class PolarsLazyGroupBy:
     def __init__(self, df: Any, keys: list[str]) -> None:
         self._compliant_frame = df
         self.keys = keys
-        self._grouped = df._native_dataframe.group_by(keys)
+        self._grouped = df._native_frame.group_by(keys)
 
     def agg(self, *aggs: Any, **named_aggs: Any) -> PolarsLazyFrame:
         aggs, named_aggs = extract_args_kwargs(aggs, named_aggs)  # type: ignore[assignment]
-        return self._compliant_frame._from_native_dataframe(  # type: ignore[no-any-return]
+        return self._compliant_frame._from_native_frame(  # type: ignore[no-any-return]
             self._grouped.agg(*aggs, **named_aggs),
         )
diff --git a/narwhals/_polars/namespace.py b/narwhals/_polars/namespace.py
index 683b83c1..ef6b0073 100644
--- a/narwhals/_polars/namespace.py
+++ b/narwhals/_polars/namespace.py
@@ -7,7 +7,7 @@ from typing import Sequence
 
 from narwhals import dtypes
 from narwhals._polars.utils import extract_args_kwargs
-from narwhals._polars.utils import reverse_translate_dtype
+from narwhals._polars.utils import narwhals_to_native_dtype
 from narwhals.dependencies import get_polars
 from narwhals.utils import Implementation
 
@@ -71,7 +71,7 @@ class PolarsNamespace:
         from narwhals._polars.dataframe import PolarsLazyFrame
 
         pl = get_polars()
-        dfs: list[Any] = [item._native_dataframe for item in items]
+        dfs: list[Any] = [item._native_frame for item in items]
         result = pl.concat(dfs, how=how)
         if isinstance(result, pl.DataFrame):
             return PolarsDataFrame(result, backend_version=items[0]._backend_version)
@@ -82,7 +82,7 @@ class PolarsNamespace:
 
         pl = get_polars()
         if dtype is not None:
-            return PolarsExpr(pl.lit(value, dtype=reverse_translate_dtype(dtype)))
+            return PolarsExpr(pl.lit(value, dtype=narwhals_to_native_dtype(dtype)))
         return PolarsExpr(pl.lit(value))
 
     def mean(self, *column_names: str) -> Any:
@@ -102,7 +102,7 @@ class PolarsSelectors:
 
         pl = get_polars()
         return PolarsExpr(
-            pl.selectors.by_dtype([reverse_translate_dtype(dtype) for dtype in dtypes])
+            pl.selectors.by_dtype([narwhals_to_native_dtype(dtype) for dtype in dtypes])
         )
 
     def numeric(self) -> PolarsExpr:
diff --git a/narwhals/_polars/series.py b/narwhals/_polars/series.py
index 4a5fd2b7..e7152004 100644
--- a/narwhals/_polars/series.py
+++ b/narwhals/_polars/series.py
@@ -17,8 +17,7 @@ if TYPE_CHECKING:
     from narwhals._polars.dataframe import PolarsDataFrame
     from narwhals.dtypes import DType
 
-from narwhals._polars.namespace import PolarsNamespace
-from narwhals._polars.utils import reverse_translate_dtype
+from narwhals._polars.utils import narwhals_to_native_dtype
 from narwhals._polars.utils import translate_dtype
 
 PL = get_polars()
@@ -39,9 +38,6 @@ class PolarsSeries:
     def __native_namespace__(self) -> Any:
         return get_polars()
 
-    def __narwhals_namespace__(self) -> PolarsNamespace:
-        return PolarsNamespace(backend_version=self._backend_version)
-
     def _from_native_series(self, series: Any) -> Self:
         return self.__class__(series, backend_version=self._backend_version)
 
@@ -94,7 +90,7 @@ class PolarsSeries:
 
     def cast(self, dtype: DType) -> Self:
         ser = self._native_series
-        dtype = reverse_translate_dtype(dtype)
+        dtype = narwhals_to_native_dtype(dtype)
         return self._from_native_series(ser.cast(dtype))
 
     def __array__(self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
diff --git a/narwhals/_polars/utils.py b/narwhals/_polars/utils.py
index 2dc92b3a..51f0b189 100644
--- a/narwhals/_polars/utils.py
+++ b/narwhals/_polars/utils.py
@@ -13,7 +13,7 @@ def extract_native(obj: Any) -> Any:
     from narwhals._polars.series import PolarsSeries
 
     if isinstance(obj, (PolarsDataFrame, PolarsLazyFrame)):
-        return obj._native_dataframe
+        return obj._native_frame
     if isinstance(obj, PolarsSeries):
         return obj._native_series
     if isinstance(obj, PolarsExpr):
@@ -68,7 +68,7 @@ def translate_dtype(dtype: Any) -> dtypes.DType:
     return dtypes.Unknown()
 
 
-def reverse_translate_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
+def narwhals_to_native_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
     pl = get_polars()
     from narwhals import dtypes
 
@@ -100,8 +100,9 @@ def reverse_translate_dtype(dtype: dtypes.DType | type[dtypes.DType]) -> Any:
         return pl.Object()
     if dtype == dtypes.Categorical:
         return pl.Categorical()
-    if dtype == dtypes.Enum:  # pragma: no cover
-        return pl.Enum()
+    if dtype == dtypes.Enum:
+        msg = "Converting to Enum is not (yet) supported"
+        raise NotImplementedError(msg)
     if dtype == dtypes.Datetime:
         return pl.Datetime()
     if dtype == dtypes.Duration:
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 487767c3..9aa7bb64 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -11,16 +11,19 @@ from typing import Sequence
 from typing import TypeVar
 from typing import overload
 
-from narwhals.dependencies import get_numpy
 from narwhals.dependencies import get_polars
+from narwhals.dependencies import is_numpy_array
 from narwhals.schema import Schema
 from narwhals.utils import flatten
+from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     from io import BytesIO
     from pathlib import Path
 
     import numpy as np
+    import pandas as pd
+    import pyarrow as pa
     from typing_extensions import Self
 
     from narwhals.group_by import GroupBy
@@ -137,9 +140,9 @@ class BaseFrame(Generic[FrameT]):
     def tail(self, n: int) -> Self:
         return self._from_compliant_dataframe(self._compliant_frame.tail(n))
 
-    def drop(self, *columns: str | Iterable[str]) -> Self:
+    def drop(self, *columns: Iterable[str], strict: bool) -> Self:
         return self._from_compliant_dataframe(
-            self._compliant_frame.drop(*flatten(columns))
+            self._compliant_frame.drop(columns, strict=strict)
         )
 
     def unique(
@@ -247,6 +250,30 @@ class DataFrame(BaseFrame[FrameT]):
             + "┘"
         )
 
+    def __arrow_c_stream__(self, requested_schema: object | None = None) -> object:
+        """
+        Export a DataFrame via the Arrow PyCapsule Interface.
+
+        - if the underlying dataframe implements the interface, it'll return that
+        - else, it'll call `to_arrow` and then defer to PyArrow's implementation
+
+        See [PyCapsule Interface](https://arrow.apache.org/docs/dev/format/CDataInterface/PyCapsuleInterface.html)
+        for more.
+        """
+        native_frame = self._compliant_frame._native_frame
+        if hasattr(native_frame, "__arrow_c_stream__"):
+            return native_frame.__arrow_c_stream__(requested_schema=requested_schema)
+        try:
+            import pyarrow as pa  # ignore-banned-import
+        except ModuleNotFoundError as exc:  # pragma: no cover
+            msg = f"PyArrow>=14.0.0 is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
+            raise ModuleNotFoundError(msg) from exc
+        if parse_version(pa.__version__) < (14, 0):  # pragma: no cover
+            msg = f"PyArrow>=14.0.0 is required for `DataFrame.__arrow_c_stream__` for object of type {type(native_frame)}"
+            raise ModuleNotFoundError(msg) from None
+        pa_table = self.to_arrow()
+        return pa_table.__arrow_c_stream__(requested_schema=requested_schema)
+
     def lazy(self) -> LazyFrame[Any]:
         """
         Lazify the DataFrame (if possible).
@@ -281,7 +308,7 @@ class DataFrame(BaseFrame[FrameT]):
         """
         return super().lazy()
 
-    def to_pandas(self) -> Any:
+    def to_pandas(self) -> pd.DataFrame:
         """
         Convert this DataFrame to a pandas DataFrame.
 
@@ -343,7 +370,7 @@ class DataFrame(BaseFrame[FrameT]):
         """
         self._compliant_frame.write_parquet(file)
 
-    def to_numpy(self) -> Any:
+    def to_numpy(self) -> np.ndarray:
         """
         Convert this DataFrame to a NumPy ndarray.
 
@@ -551,9 +578,7 @@ class DataFrame(BaseFrame[FrameT]):
             )
 
         elif isinstance(item, (Sequence, slice)) or (
-            (np := get_numpy()) is not None
-            and isinstance(item, np.ndarray)
-            and item.ndim == 1
+            is_numpy_array(item) and item.ndim == 1
         ):
             return self._from_compliant_dataframe(self._compliant_frame[item])
 
@@ -1236,7 +1261,6 @@ class DataFrame(BaseFrame[FrameT]):
             │ 3   ┆ 8   ┆ c   │
             └─────┴─────┴─────┘
         """
-
         return super().head(n)
 
     def tail(self, n: int = 5) -> Self:
@@ -1286,12 +1310,14 @@ class DataFrame(BaseFrame[FrameT]):
         """
         return super().tail(n)
 
-    def drop(self, *columns: str | Iterable[str]) -> Self:
+    def drop(self, *columns: str | Iterable[str], strict: bool = True) -> Self:
         """
         Remove columns from the dataframe.
 
         Arguments:
             *columns: Names of the columns that should be removed from the dataframe.
+            strict: Validate that all column names exist in the schema and throw an
+                exception if a column name does not exist in the schema.
 
         Examples:
             >>> import pandas as pd
@@ -1349,7 +1375,7 @@ class DataFrame(BaseFrame[FrameT]):
             │ 8.0 │
             └─────┘
         """
-        return super().drop(*columns)
+        return super().drop(*flatten(columns), strict=strict)
 
     def unique(
         self,
@@ -1806,7 +1832,6 @@ class DataFrame(BaseFrame[FrameT]):
             >>> func(df_pd), func(df_pl)
             (False, False)
         """
-
         return self._compliant_frame.is_empty()  # type: ignore[no-any-return]
 
     def is_unique(self: Self) -> Series:
@@ -1912,7 +1937,6 @@ class DataFrame(BaseFrame[FrameT]):
             │ 1   ┆ 1   ┆ 0   │
             └─────┴─────┴─────┘
         """
-
         return self._from_compliant_dataframe(self._compliant_frame.null_count())
 
     def item(self: Self, row: int | None = None, column: int | str | None = None) -> Any:
@@ -2024,7 +2048,7 @@ class DataFrame(BaseFrame[FrameT]):
         """
         return super().gather_every(n=n, offset=offset)
 
-    def to_arrow(self: Self) -> Any:
+    def to_arrow(self: Self) -> pa.Table:
         r"""
         Convert to arrow table.
 
@@ -2743,13 +2767,19 @@ class LazyFrame(BaseFrame[FrameT]):
         """
         return super().tail(n)
 
-    def drop(self, *columns: str | Iterable[str]) -> Self:
+    def drop(self, *columns: str | Iterable[str], strict: bool = True) -> Self:
         r"""
         Remove columns from the LazyFrame.
 
         Arguments:
-            *columns: Names of the columns that should be removed from the
-                      dataframe. Accepts column selector input.
+            *columns: Names of the columns that should be removed from the dataframe.
+            strict: Validate that all column names exist in the schema and throw an
+                exception if a column name does not exist in the schema.
+
+        Warning:
+            `strict` argument is ignored for `polars<1.0.0`.
+
+            Please consider upgrading to a newer version or pass to eager mode.
 
         Examples:
             >>> import pandas as pd
@@ -2807,7 +2837,7 @@ class LazyFrame(BaseFrame[FrameT]):
             │ 8.0 │
             └─────┘
         """
-        return super().drop(*flatten(columns))
+        return super().drop(*flatten(columns), strict=strict)
 
     def unique(
         self,
diff --git a/narwhals/dependencies.py b/narwhals/dependencies.py
index 9eebdb70..e2d67f03 100644
--- a/narwhals/dependencies.py
+++ b/narwhals/dependencies.py
@@ -8,11 +8,18 @@ from typing import TYPE_CHECKING
 from typing import Any
 
 if TYPE_CHECKING:
+    import numpy as np
+
     if sys.version_info >= (3, 10):
         from typing import TypeGuard
     else:
         from typing_extensions import TypeGuard
+    import cudf
+    import dask.dataframe as dd
+    import modin.pandas as mpd
     import pandas as pd
+    import polars as pl
+    import pyarrow as pa
 
 
 def get_polars() -> Any:
@@ -42,24 +49,6 @@ def get_pyarrow() -> Any:  # pragma: no cover
     return sys.modules.get("pyarrow", None)
 
 
-def get_pyarrow_compute() -> Any:  # pragma: no cover
-    """Get pyarrow.compute module (if pyarrow has already been imported - else return None)."""
-    if "pyarrow" in sys.modules:
-        import pyarrow.compute as pc
-
-        return pc
-    return None
-
-
-def get_pyarrow_parquet() -> Any:  # pragma: no cover
-    """Get pyarrow.parquet module (if pyarrow has already been imported - else return None)."""
-    if "pyarrow" in sys.modules:
-        import pyarrow.parquet as pp
-
-        return pp
-    return None
-
-
 def get_numpy() -> Any:
     """Get numpy module (if already imported - else return None)."""
     return sys.modules.get("numpy", None)
@@ -85,13 +74,72 @@ def is_pandas_dataframe(df: Any) -> TypeGuard[pd.DataFrame]:
     return bool((pd := get_pandas()) is not None and isinstance(df, pd.DataFrame))
 
 
+def is_pandas_series(ser: Any) -> TypeGuard[pd.Series[Any]]:
+    """Check whether `ser` is a pandas Series without importing pandas."""
+    return bool((pd := get_pandas()) is not None and isinstance(ser, pd.Series))
+
+
+def is_modin_dataframe(df: Any) -> TypeGuard[mpd.DataFrame]:
+    """Check whether `df` is a modin DataFrame without importing modin."""
+    return bool((pd := get_modin()) is not None and isinstance(df, pd.DataFrame))
+
+
+def is_modin_series(ser: Any) -> TypeGuard[mpd.Series]:
+    """Check whether `ser` is a modin Series without importing modin."""
+    return bool((pd := get_modin()) is not None and isinstance(ser, pd.Series))
+
+
+def is_cudf_dataframe(df: Any) -> TypeGuard[cudf.DataFrame]:
+    """Check whether `df` is a cudf DataFrame without importing cudf."""
+    return bool((pd := get_cudf()) is not None and isinstance(df, pd.DataFrame))
+
+
+def is_cudf_series(ser: Any) -> TypeGuard[pd.Series[Any]]:
+    """Check whether `ser` is a cudf Series without importing cudf."""
+    return bool((pd := get_cudf()) is not None and isinstance(ser, pd.Series))
+
+
+def is_dask_dataframe(df: Any) -> TypeGuard[dd.DataFrame]:
+    """Check whether `df` is a Dask DataFrame without importing Dask."""
+    return bool((dd := get_dask_dataframe()) is not None and isinstance(df, dd.DataFrame))
+
+
+def is_polars_dataframe(df: Any) -> TypeGuard[pl.DataFrame]:
+    """Check whether `df` is a Polars DataFrame without importing Polars."""
+    return bool((pl := get_polars()) is not None and isinstance(df, pl.DataFrame))
+
+
+def is_polars_lazyframe(df: Any) -> TypeGuard[pl.LazyFrame]:
+    """Check whether `df` is a Polars LazyFrame without importing Polars."""
+    return bool((pl := get_polars()) is not None and isinstance(df, pl.LazyFrame))
+
+
+def is_polars_series(ser: Any) -> TypeGuard[pl.Series]:
+    """Check whether `ser` is a Polars Series without importing Polars."""
+    return bool((pl := get_polars()) is not None and isinstance(ser, pl.Series))
+
+
+def is_pyarrow_chunked_array(ser: Any) -> TypeGuard[pa.ChunkedArray]:
+    """Check whether `ser` is a PyArrow ChunkedArray without importing PyArrow."""
+    return bool((pa := get_pyarrow()) is not None and isinstance(ser, pa.ChunkedArray))
+
+
+def is_pyarrow_table(df: Any) -> TypeGuard[pa.Table]:
+    """Check whether `df` is a PyArrow Table without importing PyArrow."""
+    return bool((pa := get_pyarrow()) is not None and isinstance(df, pa.Table))
+
+
+def is_numpy_array(arr: Any) -> TypeGuard[np.ndarray]:
+    """Check whether `arr` is a NumPy Array without importing NumPy."""
+    return bool((np := get_numpy()) is not None and isinstance(arr, np.ndarray))
+
+
 __all__ = [
     "get_polars",
     "get_pandas",
     "get_modin",
     "get_cudf",
     "get_pyarrow",
-    "get_pyarrow_compute",
     "get_numpy",
     "is_pandas_dataframe",
 ]
diff --git a/narwhals/dtypes.py b/narwhals/dtypes.py
index 9b56d614..4d8da429 100644
--- a/narwhals/dtypes.py
+++ b/narwhals/dtypes.py
@@ -1,9 +1,6 @@
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
-from typing import Any
-
-from narwhals.utils import isinstance_or_issubclass
 
 if TYPE_CHECKING:
     from typing_extensions import Self
@@ -18,6 +15,8 @@ class DType:
         return issubclass(cls, NumericType)
 
     def __eq__(self, other: DType | type[DType]) -> bool:  # type: ignore[override]
+        from narwhals.utils import isinstance_or_issubclass
+
         return isinstance_or_issubclass(other, type(self))
 
     def __hash__(self) -> int:
@@ -85,51 +84,3 @@ class Enum(DType): ...
 
 
 class Date(TemporalType): ...
-
-
-def translate_dtype(plx: Any, dtype: DType) -> Any:
-    if "polars" in str(type(dtype)):
-        msg = (
-            f"Expected Narwhals object, got: {type(dtype)}.\n\n"
-            "Perhaps you:\n"
-            "- Forgot a `nw.from_native` somewhere?\n"
-            "- Used `pl.Int64` instead of `nw.Int64`?"
-        )
-        raise TypeError(msg)
-    if dtype == Float64:
-        return plx.Float64
-    if dtype == Float32:
-        return plx.Float32
-    if dtype == Int64:
-        return plx.Int64
-    if dtype == Int32:
-        return plx.Int32
-    if dtype == Int16:
-        return plx.Int16
-    if dtype == Int8:
-        return plx.Int8
-    if dtype == UInt64:
-        return plx.UInt64
-    if dtype == UInt32:
-        return plx.UInt32
-    if dtype == UInt16:
-        return plx.UInt16
-    if dtype == UInt8:
-        return plx.UInt8
-    if dtype == String:
-        return plx.String
-    if dtype == Boolean:
-        return plx.Boolean
-    if dtype == Categorical:
-        return plx.Categorical
-    if dtype == Enum:
-        msg = "Converting to Enum is not (yet) supported"
-        raise NotImplementedError(msg)
-    if dtype == Datetime:
-        return plx.Datetime
-    if dtype == Duration:
-        return plx.Duration
-    if dtype == Date:
-        return plx.Date
-    msg = f"Unknown dtype: {dtype}"  # pragma: no cover
-    raise AssertionError(msg)
diff --git a/narwhals/expr.py b/narwhals/expr.py
index 1de0af2d..b89fb3d5 100644
--- a/narwhals/expr.py
+++ b/narwhals/expr.py
@@ -6,14 +6,13 @@ from typing import Callable
 from typing import Iterable
 from typing import Literal
 
-from narwhals.dependencies import get_numpy
-from narwhals.dtypes import DType
-from narwhals.dtypes import translate_dtype
+from narwhals.dependencies import is_numpy_array
 from narwhals.utils import flatten
 
 if TYPE_CHECKING:
     from typing_extensions import Self
 
+    from narwhals.dtypes import DType
     from narwhals.typing import IntoExpr
 
 
@@ -163,9 +162,8 @@ class Expr:
             │ 3.0 ┆ 8   │
             └─────┴─────┘
         """
-
         return self.__class__(
-            lambda plx: self._call(plx).cast(translate_dtype(plx, dtype)),
+            lambda plx: self._call(plx).cast(dtype),
         )
 
     # --- binary ---
@@ -500,7 +498,6 @@ class Expr:
             │ 1   ┆ 3   │
             └─────┴─────┘
         """
-
         return self.__class__(lambda plx: self._call(plx).min())
 
     def max(self) -> Self:
@@ -1410,7 +1407,6 @@ class Expr:
             │ false ┆ true  │
             └───────┴───────┘
         """
-
         return self.__class__(lambda plx: self._call(plx).is_unique())
 
     def null_count(self) -> Self:
@@ -1624,7 +1620,6 @@ class Expr:
             │ 2   │
             └─────┘
         """
-
         return self.__class__(lambda plx: self._call(plx).head(n))
 
     def tail(self, n: int = 10) -> Self:
@@ -1668,7 +1663,6 @@ class Expr:
             │ 9   │
             └─────┘
         """
-
         return self.__class__(lambda plx: self._call(plx).tail(n))
 
     def round(self, decimals: int = 0) -> Self:
@@ -1720,7 +1714,6 @@ class Expr:
             │ 3.9 │
             └─────┘
         """
-
         return self.__class__(lambda plx: self._call(plx).round(decimals))
 
     def len(self) -> Self:
@@ -2243,7 +2236,6 @@ class ExprStringNamespace:
             │ null              ┆ null          ┆ null                   ┆ null          │
             └───────────────────┴───────────────┴────────────────────────┴───────────────┘
         """
-
         return self._expr.__class__(
             lambda plx: self._expr._call(plx).str.contains(pattern, literal=literal)
         )
@@ -3446,7 +3438,6 @@ class ExprNameNamespace:
             >>> func(df_pl).columns
             ['foo']
         """
-
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.keep())
 
     def map(self: Self, function: Callable[[str], str]) -> Expr:
@@ -3483,7 +3474,6 @@ class ExprNameNamespace:
             >>> func(df_pl).columns
             ['oof', 'RAB']
         """
-
         return self._expr.__class__(lambda plx: self._expr._call(plx).name.map(function))
 
     def prefix(self: Self, prefix: str) -> Expr:
@@ -4054,7 +4044,7 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         └─────┴─────┘
 
     """
-    if (np := get_numpy()) is not None and isinstance(value, np.ndarray):
+    if is_numpy_array(value):
         msg = (
             "numpy arrays are not supported as literal values. "
             "Consider using `with_columns` to create a new column from the array."
@@ -4065,9 +4055,7 @@ def lit(value: Any, dtype: DType | None = None) -> Expr:
         msg = f"Nested datatypes are not supported yet. Got {value}"
         raise NotImplementedError(msg)
 
-    if dtype is None:
-        return Expr(lambda plx: plx.lit(value, dtype))
-    return Expr(lambda plx: plx.lit(value, translate_dtype(plx, dtype)))
+    return Expr(lambda plx: plx.lit(value, dtype))
 
 
 def any_horizontal(*exprs: IntoExpr | Iterable[IntoExpr]) -> Expr:
diff --git a/narwhals/functions.py b/narwhals/functions.py
index 45f273df..d5d8be4d 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -48,6 +48,100 @@ def concat(
     )
 
 
+def new_series(
+    name: str,
+    values: Any,
+    dtype: DType | type[DType] | None = None,
+    *,
+    native_namespace: ModuleType,
+) -> Series:
+    """
+    Instantiate Narwhals Series from raw data.
+
+    Arguments:
+        name: Name of resulting Series.
+        values: Values of make Series from.
+        dtype: (Narwhals) dtype. If not provided, the native library
+            may auto-infer it from `values`.
+        native_namespace: The native library to use for DataFrame creation.
+
+    Examples:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import narwhals as nw
+        >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
+
+        Let's define a dataframe-agnostic function:
+
+        >>> @nw.narwhalify
+        ... def func(df):
+        ...     values = [4, 1, 2]
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.new_series("c", values, nw.Int32, native_namespace=native_namespace)
+
+        Let's see what happens when passing pandas / Polars input:
+
+        >>> func(pd.DataFrame(data))
+        0    4
+        1    1
+        2    2
+        Name: c, dtype: int32
+        >>> func(pl.DataFrame(data))  # doctest: +NORMALIZE_WHITESPACE
+        shape: (3,)
+        Series: 'c' [i32]
+        [
+           4
+           1
+           2
+        ]
+    """
+    implementation = Implementation.from_native_namespace(native_namespace)
+
+    if implementation is Implementation.POLARS:
+        if dtype:
+            from narwhals._polars.utils import (
+                narwhals_to_native_dtype as polars_narwhals_to_native_dtype,
+            )
+
+            dtype = polars_narwhals_to_native_dtype(dtype)
+
+        native_series = native_namespace.Series(name=name, values=values, dtype=dtype)
+    elif implementation in {
+        Implementation.PANDAS,
+        Implementation.MODIN,
+        Implementation.CUDF,
+    }:
+        if dtype:
+            from narwhals._pandas_like.utils import (
+                narwhals_to_native_dtype as pandas_like_narwhals_to_native_dtype,
+            )
+
+            dtype = pandas_like_narwhals_to_native_dtype(dtype, None, implementation)
+        native_series = native_namespace.Series(values, name=name, dtype=dtype)
+
+    elif implementation is Implementation.PYARROW:
+        if dtype:
+            from narwhals._arrow.utils import (
+                narwhals_to_native_dtype as arrow_narwhals_to_native_dtype,
+            )
+
+            dtype = arrow_narwhals_to_native_dtype(dtype)
+        native_series = native_namespace.chunked_array([values], type=dtype)
+
+    elif implementation is Implementation.DASK:
+        msg = "Dask support in Narwhals is lazy-only, so `new_series` is " "not supported"
+        raise NotImplementedError(msg)
+    else:  # pragma: no cover
+        try:
+            # implementation is UNKNOWN, Narhwals extension using this feature should
+            # implement `from_dict` function in the top-level namespace.
+            native_series = native_namespace.new_series(name, values, dtype)
+        except AttributeError as e:
+            msg = "Unknown namespace is expected to implement `Series` constructor."
+            raise AttributeError(msg) from e
+    return from_native(native_series, series_only=True).alias(name)
+
+
 def from_dict(
     data: dict[str, Any],
     schema: dict[str, DType] | Schema | None = None,
@@ -118,11 +212,11 @@ def from_dict(
     if implementation is Implementation.POLARS:
         if schema:
             from narwhals._polars.utils import (
-                reverse_translate_dtype as polars_reverse_translate_dtype,
+                narwhals_to_native_dtype as polars_narwhals_to_native_dtype,
             )
 
             schema = {
-                name: polars_reverse_translate_dtype(dtype)
+                name: polars_narwhals_to_native_dtype(dtype)
                 for name, dtype in schema.items()
             }
 
@@ -136,11 +230,11 @@ def from_dict(
 
         if schema:
             from narwhals._pandas_like.utils import (
-                reverse_translate_dtype as pandas_like_reverse_translate_dtype,
+                narwhals_to_native_dtype as pandas_like_narwhals_to_native_dtype,
             )
 
             schema = {
-                name: pandas_like_reverse_translate_dtype(
+                name: pandas_like_narwhals_to_native_dtype(
                     schema[name], native_type, implementation
                 )
                 for name, native_type in native_frame.dtypes.items()
@@ -150,12 +244,12 @@ def from_dict(
     elif implementation is Implementation.PYARROW:
         if schema:
             from narwhals._arrow.utils import (
-                reverse_translate_dtype as arrow_reverse_translate_dtype,
+                narwhals_to_native_dtype as arrow_narwhals_to_native_dtype,
             )
 
             schema = native_namespace.schema(
                 [
-                    (name, arrow_reverse_translate_dtype(dtype))
+                    (name, arrow_narwhals_to_native_dtype(dtype))
                     for name, dtype in schema.items()
                 ]
             )
diff --git a/narwhals/selectors.py b/narwhals/selectors.py
index fca1c4cd..7c06a79c 100644
--- a/narwhals/selectors.py
+++ b/narwhals/selectors.py
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 from typing import Any
 
-from narwhals.dtypes import translate_dtype
 from narwhals.expr import Expr
 from narwhals.utils import flatten
 
@@ -51,11 +50,7 @@ def by_dtype(*dtypes: Any) -> Expr:
         │ 4   ┆ 4.6 │
         └─────┴─────┘
     """
-    return Selector(
-        lambda plx: plx.selectors.by_dtype(
-            [translate_dtype(plx, dtype) for dtype in flatten(dtypes)]
-        )
-    )
+    return Selector(lambda plx: plx.selectors.by_dtype(flatten(dtypes)))
 
 
 def numeric() -> Expr:
diff --git a/narwhals/series.py b/narwhals/series.py
index ca3fbbb0..a1bcae18 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -7,10 +7,12 @@ from typing import Literal
 from typing import Sequence
 from typing import overload
 
-from narwhals.dtypes import translate_dtype
+from narwhals.utils import parse_version
 
 if TYPE_CHECKING:
     import numpy as np
+    import pandas as pd
+    import pyarrow as pa
     from typing_extensions import Self
 
     from narwhals.dataframe import DataFrame
@@ -57,8 +59,31 @@ class Series:
     def __native_namespace__(self) -> Any:
         return self._compliant_series.__native_namespace__()
 
-    def __narwhals_namespace__(self) -> Any:
-        return self._compliant_series.__narwhals_namespace__()
+    def __arrow_c_stream__(self, requested_schema: object | None = None) -> object:
+        """
+        Export a Series via the Arrow PyCapsule Interface.
+
+        Narwhals doesn't implement anything itself here:
+
+        - if the underlying series implements the interface, it'll return that
+        - else, it'll call `to_arrow` and then defer to PyArrow's implementation
+
+        See [PyCapsule Interface](https://arrow.apache.org/docs/dev/format/CDataInterface/PyCapsuleInterface.html)
+        for more.
+        """
+        native_series = self._compliant_series._native_series
+        if hasattr(native_series, "__arrow_c_stream__"):
+            return native_series.__arrow_c_stream__(requested_schema=requested_schema)
+        try:
+            import pyarrow as pa  # ignore-banned-import
+        except ModuleNotFoundError as exc:  # pragma: no cover
+            msg = f"PyArrow>=16.0.0 is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
+            raise ModuleNotFoundError(msg) from exc
+        if parse_version(pa.__version__) < (16, 0):  # pragma: no cover
+            msg = f"PyArrow>=16.0.0 is required for `Series.__arrow_c_stream__` for object of type {type(native_series)}"
+            raise ModuleNotFoundError(msg)
+        ca = pa.chunked_array([self.to_arrow()])
+        return ca.__arrow_c_stream__(requested_schema=requested_schema)
 
     @property
     def shape(self) -> tuple[int]:
@@ -281,11 +306,7 @@ class Series:
                1
             ]
         """
-        return self._from_compliant_series(
-            self._compliant_series.cast(
-                translate_dtype(self.__narwhals_namespace__(), dtype)
-            )
-        )
+        return self._from_compliant_series(self._compliant_series.cast(dtype))
 
     def to_frame(self) -> DataFrame[Any]:
         """
@@ -1331,7 +1352,7 @@ class Series:
         """
         return self._compliant_series.n_unique()  # type: ignore[no-any-return]
 
-    def to_numpy(self) -> Any:
+    def to_numpy(self) -> np.ndarray:
         """
         Convert to numpy.
 
@@ -1358,7 +1379,7 @@ class Series:
         """
         return self._compliant_series.to_numpy()
 
-    def to_pandas(self) -> Any:
+    def to_pandas(self) -> pd.Series:
         """
         Convert to pandas.
 
@@ -1677,7 +1698,6 @@ class Series:
             >>> func(s_pl)
             2
         """
-
         return self._compliant_series.null_count()  # type: ignore[no-any-return]
 
     def is_first_distinct(self: Self) -> Self:
@@ -1948,7 +1968,6 @@ class Series:
             4    5
             dtype: int64
         """
-
         return self._from_compliant_series(
             self._compliant_series.zip_with(
                 self._extract_native(mask), self._extract_native(other)
@@ -2022,7 +2041,6 @@ class Series:
                2
             ]
         """
-
         return self._from_compliant_series(self._compliant_series.head(n))
 
     def tail(self: Self, n: int = 10) -> Self:
@@ -2063,7 +2081,6 @@ class Series:
                9
             ]
         """
-
         return self._from_compliant_series(self._compliant_series.tail(n))
 
     def round(self: Self, decimals: int = 0) -> Self:
@@ -2179,7 +2196,6 @@ class Series:
             │ 0   ┆ 1   │
             └─────┴─────┘
         """
-
         from narwhals.dataframe import DataFrame
 
         return DataFrame(
@@ -2227,7 +2243,7 @@ class Series:
             self._compliant_series.gather_every(n=n, offset=offset)
         )
 
-    def to_arrow(self: Self) -> Any:
+    def to_arrow(self: Self) -> pa.Array:
         r"""
         Convert to arrow.
 
@@ -2263,7 +2279,6 @@ class Series:
                 4
             ]
         """
-
         return self._compliant_series.to_arrow()
 
     @property
diff --git a/narwhals/stable/v1.py b/narwhals/stable/v1.py
index 1c1c9171..b5697753 100644
--- a/narwhals/stable/v1.py
+++ b/narwhals/stable/v1.py
@@ -1469,6 +1469,58 @@ def get_level(
     return nw.get_level(obj)
 
 
+def new_series(
+    name: str,
+    values: Any,
+    dtype: DType | type[DType] | None = None,
+    *,
+    native_namespace: ModuleType,
+) -> Series:
+    """
+    Instantiate Narwhals Series from raw data.
+
+    Arguments:
+        name: Name of resulting Series.
+        values: Values of make Series from.
+        dtype: (Narwhals) dtype. If not provided, the native library
+            may auto-infer it from `values`.
+        native_namespace: The native library to use for DataFrame creation.
+
+    Examples:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import narwhals.stable.v1 as nw
+        >>> data = {"a": [1, 2, 3], "b": [4, 5, 6]}
+
+        Let's define a dataframe-agnostic function:
+
+        >>> @nw.narwhalify
+        ... def func(df):
+        ...     values = [4, 1, 2]
+        ...     native_namespace = nw.get_native_namespace(df)
+        ...     return nw.new_series("c", values, nw.Int32, native_namespace=native_namespace)
+
+        Let's see what happens when passing pandas / Polars input:
+
+        >>> func(pd.DataFrame(data))
+        0    4
+        1    1
+        2    2
+        Name: c, dtype: int32
+        >>> func(pl.DataFrame(data))  # doctest: +NORMALIZE_WHITESPACE
+        shape: (3,)
+        Series: 'c' [i32]
+        [
+           4
+           1
+           2
+        ]
+    """
+    return _stableify(
+        nw.new_series(name, values, dtype, native_namespace=native_namespace)
+    )
+
+
 def from_dict(
     data: dict[str, Any],
     schema: dict[str, DType] | Schema | None = None,
@@ -1573,4 +1625,5 @@ __all__ = [
     "show_versions",
     "Schema",
     "from_dict",
+    "new_series",
 ]
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 00c2b05f..f396a898 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -10,12 +10,23 @@ from typing import overload
 
 from narwhals.dependencies import get_cudf
 from narwhals.dependencies import get_dask
-from narwhals.dependencies import get_dask_dataframe
 from narwhals.dependencies import get_dask_expr
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_polars
 from narwhals.dependencies import get_pyarrow
+from narwhals.dependencies import is_cudf_dataframe
+from narwhals.dependencies import is_cudf_series
+from narwhals.dependencies import is_dask_dataframe
+from narwhals.dependencies import is_modin_dataframe
+from narwhals.dependencies import is_modin_series
+from narwhals.dependencies import is_pandas_dataframe
+from narwhals.dependencies import is_pandas_series
+from narwhals.dependencies import is_polars_dataframe
+from narwhals.dependencies import is_polars_lazyframe
+from narwhals.dependencies import is_polars_series
+from narwhals.dependencies import is_pyarrow_chunked_array
+from narwhals.dependencies import is_pyarrow_table
 
 if TYPE_CHECKING:
     from narwhals.dataframe import DataFrame
@@ -60,7 +71,7 @@ def to_native(
     from narwhals.series import Series
 
     if isinstance(narwhals_object, BaseFrame):
-        return narwhals_object._compliant_frame._native_dataframe
+        return narwhals_object._compliant_frame._native_frame
     if isinstance(narwhals_object, Series):
         return narwhals_object._compliant_series._native_series
 
@@ -340,27 +351,33 @@ def from_native(  # noqa: PLR0915
             level="full",
         )
 
+    # TODO(marco): write all of these in terms of `is_` rather
+    # than `get_` + walrus
+
     # Polars
-    elif (pl := get_polars()) is not None and isinstance(native_object, pl.DataFrame):
+    elif is_polars_dataframe(native_object):
         if series_only:
             msg = "Cannot only use `series_only` with polars.DataFrame"
             raise TypeError(msg)
+        pl = get_polars()
         return DataFrame(
             PolarsDataFrame(native_object, backend_version=parse_version(pl.__version__)),
             level="full",
         )
-    elif (pl := get_polars()) is not None and isinstance(native_object, pl.LazyFrame):
+    elif is_polars_lazyframe(native_object):
         if series_only:
             msg = "Cannot only use `series_only` with polars.LazyFrame"
             raise TypeError(msg)
         if eager_only or eager_or_interchange_only:
             msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with polars.LazyFrame"
             raise TypeError(msg)
+        pl = get_polars()
         return LazyFrame(
             PolarsLazyFrame(native_object, backend_version=parse_version(pl.__version__)),
             level="full",
         )
-    elif (pl := get_polars()) is not None and isinstance(native_object, pl.Series):
+    elif is_polars_series(native_object):
+        pl = get_polars()
         if not allow_series:
             msg = "Please set `allow_series=True`"
             raise TypeError(msg)
@@ -370,10 +387,11 @@ def from_native(  # noqa: PLR0915
         )
 
     # pandas
-    elif (pd := get_pandas()) is not None and isinstance(native_object, pd.DataFrame):
+    elif is_pandas_dataframe(native_object):
         if series_only:
             msg = "Cannot only use `series_only` with dataframe"
             raise TypeError(msg)
+        pd = get_pandas()
         return DataFrame(
             PandasLikeDataFrame(
                 native_object,
@@ -382,10 +400,11 @@ def from_native(  # noqa: PLR0915
             ),
             level="full",
         )
-    elif (pd := get_pandas()) is not None and isinstance(native_object, pd.Series):
+    elif is_pandas_series(native_object):
         if not allow_series:
             msg = "Please set `allow_series=True`"
             raise TypeError(msg)
+        pd = get_pandas()
         return Series(
             PandasLikeSeries(
                 native_object,
@@ -396,9 +415,8 @@ def from_native(  # noqa: PLR0915
         )
 
     # Modin
-    elif (mpd := get_modin()) is not None and isinstance(
-        native_object, mpd.DataFrame
-    ):  # pragma: no cover
+    elif is_modin_dataframe(native_object):  # pragma: no cover
+        mpd = get_modin()
         if series_only:
             msg = "Cannot only use `series_only` with modin.DataFrame"
             raise TypeError(msg)
@@ -410,9 +428,8 @@ def from_native(  # noqa: PLR0915
             ),
             level="full",
         )
-    elif (mpd := get_modin()) is not None and isinstance(
-        native_object, mpd.Series
-    ):  # pragma: no cover
+    elif is_modin_series(native_object):  # pragma: no cover
+        mpd = get_modin()
         if not allow_series:
             msg = "Please set `allow_series=True`"
             raise TypeError(msg)
@@ -426,9 +443,8 @@ def from_native(  # noqa: PLR0915
         )
 
     # cuDF
-    elif (cudf := get_cudf()) is not None and isinstance(  # pragma: no cover
-        native_object, cudf.DataFrame
-    ):
+    elif is_cudf_dataframe(native_object):  # pragma: no cover
+        cudf = get_cudf()
         if series_only:
             msg = "Cannot only use `series_only` with cudf.DataFrame"
             raise TypeError(msg)
@@ -440,9 +456,8 @@ def from_native(  # noqa: PLR0915
             ),
             level="full",
         )
-    elif (cudf := get_cudf()) is not None and isinstance(
-        native_object, cudf.Series
-    ):  # pragma: no cover
+    elif is_cudf_series(native_object):  # pragma: no cover
+        cudf = get_cudf()
         if not allow_series:
             msg = "Please set `allow_series=True`"
             raise TypeError(msg)
@@ -456,7 +471,8 @@ def from_native(  # noqa: PLR0915
         )
 
     # PyArrow
-    elif (pa := get_pyarrow()) is not None and isinstance(native_object, pa.Table):
+    elif is_pyarrow_table(native_object):
+        pa = get_pyarrow()
         if series_only:
             msg = "Cannot only use `series_only` with arrow table"
             raise TypeError(msg)
@@ -464,7 +480,8 @@ def from_native(  # noqa: PLR0915
             ArrowDataFrame(native_object, backend_version=parse_version(pa.__version__)),
             level="full",
         )
-    elif (pa := get_pyarrow()) is not None and isinstance(native_object, pa.ChunkedArray):
+    elif is_pyarrow_chunked_array(native_object):
+        pa = get_pyarrow()
         if not allow_series:
             msg = "Please set `allow_series=True`"
             raise TypeError(msg)
@@ -476,9 +493,7 @@ def from_native(  # noqa: PLR0915
         )
 
     # Dask
-    elif (dd := get_dask_dataframe()) is not None and isinstance(
-        native_object, dd.DataFrame
-    ):
+    elif is_dask_dataframe(native_object):
         if series_only:
             msg = "Cannot only use `series_only` with dask DataFrame"
             raise TypeError(msg)
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 2034c6fe..cc2a482c 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -12,11 +12,19 @@ from typing import TypeVar
 from typing import cast
 
 from narwhals import dtypes
+from narwhals._exceptions import ColumnNotFoundError
 from narwhals.dependencies import get_cudf
+from narwhals.dependencies import get_dask_dataframe
 from narwhals.dependencies import get_modin
 from narwhals.dependencies import get_pandas
 from narwhals.dependencies import get_polars
 from narwhals.dependencies import get_pyarrow
+from narwhals.dependencies import is_cudf_series
+from narwhals.dependencies import is_modin_series
+from narwhals.dependencies import is_pandas_dataframe
+from narwhals.dependencies import is_pandas_series
+from narwhals.dependencies import is_polars_series
+from narwhals.dependencies import is_pyarrow_chunked_array
 from narwhals.translate import to_native
 
 if TYPE_CHECKING:
@@ -36,6 +44,7 @@ class Implementation(Enum):
     CUDF = auto()
     PYARROW = auto()
     POLARS = auto()
+    DASK = auto()
 
     UNKNOWN = auto()
 
@@ -50,6 +59,7 @@ class Implementation(Enum):
             get_cudf(): Implementation.CUDF,
             get_pyarrow(): Implementation.PYARROW,
             get_polars(): Implementation.POLARS,
+            get_dask_dataframe(): Implementation.DASK,
         }
         return mapping.get(native_namespace, Implementation.UNKNOWN)
 
@@ -94,7 +104,7 @@ def tupleify(arg: Any) -> Any:
 def _is_iterable(arg: Any | Iterable[Any]) -> bool:
     from narwhals.series import Series
 
-    if (pd := get_pandas()) is not None and isinstance(arg, (pd.Series, pd.DataFrame)):
+    if is_pandas_dataframe(arg) or is_pandas_series(arg):
         msg = f"Expected Narwhals class or scalar, got: {type(arg)}. Perhaps you forgot a `nw.from_native` somewhere?"
         raise TypeError(msg)
     if (pl := get_polars()) is not None and isinstance(
@@ -177,23 +187,23 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
     if isinstance(
         getattr(lhs_any, "_compliant_frame", None), PandasLikeDataFrame
     ) and isinstance(getattr(rhs_any, "_compliant_frame", None), PandasLikeDataFrame):
-        _validate_index(lhs_any._compliant_frame._native_dataframe.index)
-        _validate_index(rhs_any._compliant_frame._native_dataframe.index)
+        _validate_index(lhs_any._compliant_frame._native_frame.index)
+        _validate_index(rhs_any._compliant_frame._native_frame.index)
         return lhs_any._from_compliant_dataframe(  # type: ignore[no-any-return]
-            lhs_any._compliant_frame._from_native_dataframe(
-                lhs_any._compliant_frame._native_dataframe.loc[
-                    rhs_any._compliant_frame._native_dataframe.index
+            lhs_any._compliant_frame._from_native_frame(
+                lhs_any._compliant_frame._native_frame.loc[
+                    rhs_any._compliant_frame._native_frame.index
                 ]
             )
         )
     if isinstance(
         getattr(lhs_any, "_compliant_frame", None), PandasLikeDataFrame
     ) and isinstance(getattr(rhs_any, "_compliant_series", None), PandasLikeSeries):
-        _validate_index(lhs_any._compliant_frame._native_dataframe.index)
+        _validate_index(lhs_any._compliant_frame._native_frame.index)
         _validate_index(rhs_any._compliant_series._native_series.index)
         return lhs_any._from_compliant_dataframe(  # type: ignore[no-any-return]
-            lhs_any._compliant_frame._from_native_dataframe(
-                lhs_any._compliant_frame._native_dataframe.loc[
+            lhs_any._compliant_frame._from_native_frame(
+                lhs_any._compliant_frame._native_frame.loc[
                     rhs_any._compliant_series._native_series.index
                 ]
             )
@@ -202,11 +212,11 @@ def maybe_align_index(lhs: T, rhs: Series | BaseFrame[Any]) -> T:
         getattr(lhs_any, "_compliant_series", None), PandasLikeSeries
     ) and isinstance(getattr(rhs_any, "_compliant_frame", None), PandasLikeDataFrame):
         _validate_index(lhs_any._compliant_series._native_series.index)
-        _validate_index(rhs_any._compliant_frame._native_dataframe.index)
+        _validate_index(rhs_any._compliant_frame._native_frame.index)
         return lhs_any._from_compliant_series(  # type: ignore[no-any-return]
             lhs_any._compliant_series._from_native_series(
                 lhs_any._compliant_series._native_series.loc[
-                    rhs_any._compliant_frame._native_dataframe.index
+                    rhs_any._compliant_frame._native_frame.index
                 ]
             )
         )
@@ -256,8 +266,8 @@ def maybe_set_index(df: T, column_names: str | list[str]) -> T:
     df_any = cast(Any, df)
     if isinstance(getattr(df_any, "_compliant_frame", None), PandasLikeDataFrame):
         return df_any._from_compliant_dataframe(  # type: ignore[no-any-return]
-            df_any._compliant_frame._from_native_dataframe(
-                df_any._compliant_frame._native_dataframe.set_index(column_names)
+            df_any._compliant_frame._from_native_frame(
+                df_any._compliant_frame._native_frame.set_index(column_names)
             )
         )
     return df
@@ -293,8 +303,8 @@ def maybe_convert_dtypes(df: T, *args: bool, **kwargs: bool | str) -> T:
     df_any = cast(Any, df)
     if isinstance(getattr(df_any, "_compliant_frame", None), PandasLikeDataFrame):
         return df_any._from_compliant_dataframe(  # type: ignore[no-any-return]
-            df_any._compliant_frame._from_native_dataframe(
-                df_any._compliant_frame._native_dataframe.convert_dtypes(*args, **kwargs)
+            df_any._compliant_frame._from_native_frame(
+                df_any._compliant_frame._native_frame.convert_dtypes(*args, **kwargs)
             )
         )
     return df
@@ -351,19 +361,15 @@ def is_ordered_categorical(series: Series) -> bool:
     if series.dtype != dtypes.Categorical:
         return False
     native_series = to_native(series)
-    if (pl := get_polars()) is not None and isinstance(native_series, pl.Series):
-        return native_series.dtype.ordering == "physical"  # type: ignore[no-any-return]
-    if (pd := get_pandas()) is not None and isinstance(native_series, pd.Series):
+    if is_polars_series(native_series):
+        return native_series.dtype.ordering == "physical"  # type: ignore[attr-defined, no-any-return]
+    if is_pandas_series(native_series):
         return native_series.cat.ordered  # type: ignore[no-any-return]
-    if (mpd := get_modin()) is not None and isinstance(
-        native_series, mpd.Series
-    ):  # pragma: no cover
+    if is_modin_series(native_series):  # pragma: no cover
         return native_series.cat.ordered  # type: ignore[no-any-return]
-    if (cudf := get_cudf()) is not None and isinstance(
-        native_series, cudf.Series
-    ):  # pragma: no cover
+    if is_cudf_series(native_series):  # pragma: no cover
         return native_series.cat.ordered  # type: ignore[no-any-return]
-    if (pa := get_pyarrow()) is not None and isinstance(native_series, pa.ChunkedArray):
+    if is_pyarrow_chunked_array(native_series):
         return native_series.type.ordered  # type: ignore[no-any-return]
     # If it doesn't match any of the above, let's just play it safe and return False.
     return False  # pragma: no cover
@@ -395,3 +401,21 @@ def generate_unique_token(n_bytes: int, columns: list[str]) -> str:  # pragma: n
                 "join operation"
             )
             raise AssertionError(msg)
+
+
+def parse_columns_to_drop(
+    compliant_frame: Any,
+    columns: Iterable[str],
+    strict: bool,  # noqa: FBT001
+) -> list[str]:
+    cols = set(compliant_frame.columns)
+    to_drop = list(columns)
+
+    if strict:
+        for d in to_drop:
+            if d not in cols:
+                msg = f'"{d}" not found'
+                raise ColumnNotFoundError(msg)
+    else:
+        to_drop = list(cols.intersection(set(to_drop)))
+    return to_drop
diff --git a/pyproject.toml b/pyproject.toml
index 96f33400..086e3bdf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "narwhals"
-version = "1.3.0"
+version = "1.4.1"
 authors = [
   { name="Marco Gorelli", email="33491632+MarcoGorelli@users.noreply.github.com" },
 ]
@@ -55,6 +55,7 @@ lint.ignore = [
   "E501",
   "FIX",
   "ISC001",
+  "NPY002",
   "PD901",  # This is a auxiliary library so dataframe variables have no concrete business meaning
   "PLR0911",
   "PLR0912",
diff --git a/utils/check_api_reference.py b/utils/check_api_reference.py
index 68c98008..f6e5303c 100644
--- a/utils/check_api_reference.py
+++ b/utils/check_api_reference.py
@@ -45,13 +45,13 @@ with open("docs/api-reference/dataframe.md") as fd:
 documented = [
     remove_prefix(i, "        - ")
     for i in content.splitlines()
-    if i.startswith("        - ")
+    if i.startswith("        - ") and not i.startswith("        - _")
 ]
 if missing := set(top_level_functions).difference(documented):
     print("DataFrame: not documented")  # noqa: T201
     print(missing)  # noqa: T201
     ret = 1
-if extra := set(documented).difference(top_level_functions).difference({"__getitem__"}):
+if extra := set(documented).difference(top_level_functions):
     print("DataFrame: outdated")  # noqa: T201
     print(extra)  # noqa: T201
     ret = 1
@@ -87,7 +87,7 @@ with open("docs/api-reference/series.md") as fd:
 documented = [
     remove_prefix(i, "        - ")
     for i in content.splitlines()
-    if i.startswith("        - ")
+    if i.startswith("        - ") and not i.startswith("        - _")
 ]
 if (
     missing := set(top_level_functions)
diff --git a/utils/import_check.py b/utils/import_check.py
new file mode 100644
index 00000000..e8d776cd
--- /dev/null
+++ b/utils/import_check.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+import ast
+import sys
+
+BANNED_IMPORTS = {
+    "cudf",
+    "dask",
+    "dask.dataframe",
+    "dask_expr",
+    "duckdb",
+    "ibis",
+    "modin",
+    "numpy",
+    "pandas",
+    "polars",
+    "pyarrow",
+}
+
+
+class ImportPandasChecker(ast.NodeVisitor):
+    def __init__(self, file_name: str, lines: list[str]) -> None:
+        self.file_name = file_name
+        self.lines = lines
+        self.found_import = False
+
+    def visit_If(self, node: ast.If) -> None:  # noqa: N802
+        # Check if the condition is `if TYPE_CHECKING`
+        if isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING":
+            # Skip the body of this if statement
+            return
+        self.generic_visit(node)
+
+    def visit_Import(self, node: ast.Import) -> None:  # noqa: N802
+        for alias in node.names:
+            if (
+                alias.name in BANNED_IMPORTS
+                and "# ignore-banned-import" not in self.lines[node.lineno - 1]
+            ):
+                print(  # noqa: T201
+                    f"{self.file_name}:{node.lineno}:{node.col_offset}: found {alias.name} import"
+                )
+                self.found_import = True
+        self.generic_visit(node)
+
+    def visit_ImportFrom(self, node: ast.ImportFrom) -> None:  # noqa: N802
+        if (
+            node.module in BANNED_IMPORTS
+            and "# ignore-banned-import" not in self.lines[node.lineno - 1]
+        ):
+            print(  # noqa: T201
+                f"{self.file_name}:{node.lineno}:{node.col_offset}: found {node.module} import"
+            )
+            self.found_import = True
+        self.generic_visit(node)
+
+
+def check_import_pandas(filename: str) -> bool:
+    with open(filename) as file:
+        content = file.read()
+    tree = ast.parse(content, filename=filename)
+
+    checker = ImportPandasChecker(filename, content.splitlines())
+    checker.visit(tree)
+
+    return checker.found_import
+
+
+if __name__ == "__main__":
+    ret = 0
+    for filename in sys.argv[1:]:
+        if not filename.endswith(".py"):
+            continue
+        ret |= check_import_pandas(filename)
+    sys.exit(ret)


Test patch:
diff --git a/tests/frame/arrow_c_stream_test.py b/tests/frame/arrow_c_stream_test.py
new file mode 100644
index 00000000..7a3403f6
--- /dev/null
+++ b/tests/frame/arrow_c_stream_test.py
@@ -0,0 +1,42 @@
+import polars as pl
+import pyarrow as pa
+import pyarrow.compute as pc
+import pytest
+
+import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
+
+
+@pytest.mark.skipif(
+    parse_version(pl.__version__) < (1, 3), reason="too old for pycapsule in Polars"
+)
+def test_arrow_c_stream_test() -> None:
+    df = nw.from_native(pl.Series([1, 2, 3]).to_frame("a"), eager_only=True)
+    result = pa.table(df)
+    expected = pa.table({"a": [1, 2, 3]})
+    assert pc.all(pc.equal(result["a"], expected["a"])).as_py()
+
+
+@pytest.mark.skipif(
+    parse_version(pl.__version__) < (1, 3), reason="too old for pycapsule in Polars"
+)
+def test_arrow_c_stream_test_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
+    # "poison" the dunder method to make sure it actually got called above
+    monkeypatch.setattr(
+        "narwhals.dataframe.DataFrame.__arrow_c_stream__", lambda *_: 1 / 0
+    )
+    df = nw.from_native(pl.Series([1, 2, 3]).to_frame("a"), eager_only=True)
+    with pytest.raises(ZeroDivisionError, match="division by zero"):
+        pa.table(df)
+
+
+@pytest.mark.skipif(
+    parse_version(pl.__version__) < (1, 3), reason="too old for pycapsule in Polars"
+)
+def test_arrow_c_stream_test_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
+    # Check that fallback to PyArrow works
+    monkeypatch.delattr("polars.DataFrame.__arrow_c_stream__")
+    df = nw.from_native(pl.Series([1, 2, 3]).to_frame("a"), eager_only=True)
+    result = pa.table(df)
+    expected = pa.table({"a": [1, 2, 3]})
+    assert pc.all(pc.equal(result["a"], expected["a"])).as_py()
diff --git a/tests/frame/drop_test.py b/tests/frame/drop_test.py
index f22d148b..db039fcb 100644
--- a/tests/frame/drop_test.py
+++ b/tests/frame/drop_test.py
@@ -1,21 +1,56 @@
 from __future__ import annotations
 
+from contextlib import nullcontext as does_not_raise
 from typing import Any
 
+import polars as pl
 import pytest
+from polars.exceptions import ColumnNotFoundError as PlColumnNotFoundError
 
 import narwhals.stable.v1 as nw
+from narwhals._exceptions import ColumnNotFoundError
+from narwhals.utils import parse_version
 
 
 @pytest.mark.parametrize(
-    ("drop", "left"),
+    ("to_drop", "expected"),
     [
-        (["a"], ["b", "z"]),
-        (["a", "b"], ["z"]),
+        ("abc", ["b", "z"]),
+        (["abc"], ["b", "z"]),
+        (["abc", "b"], ["z"]),
     ],
 )
-def test_drop(constructor: Any, drop: list[str], left: list[str]) -> None:
-    data = {"a": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
+def test_drop(constructor: Any, to_drop: list[str], expected: list[str]) -> None:
+    data = {"abc": [1, 3, 2], "b": [4, 4, 6], "z": [7.0, 8, 9]}
     df = nw.from_native(constructor(data))
-    assert df.drop(drop).collect_schema().names() == left
-    assert df.drop(*drop).collect_schema().names() == left
+    assert df.drop(to_drop).collect_schema().names() == expected
+    if not isinstance(to_drop, str):
+        assert df.drop(*to_drop).collect_schema().names() == expected
+
+
+@pytest.mark.parametrize(
+    ("strict", "context"),
+    [
+        (
+            True,
+            pytest.raises((ColumnNotFoundError, PlColumnNotFoundError), match="z"),
+        ),
+        (False, does_not_raise()),
+    ],
+)
+def test_drop_strict(request: Any, constructor: Any, strict: bool, context: Any) -> None:  # noqa: FBT001
+    if (
+        "polars_lazy" in str(request)
+        and parse_version(pl.__version__) < (1, 0, 0)
+        and strict
+    ):
+        request.applymarker(pytest.mark.xfail)
+
+    data = {"a": [1, 3, 2], "b": [4, 4, 6]}
+    to_drop = ["a", "z"]
+
+    df = nw.from_native(constructor(data))
+
+    with context:
+        names_out = df.drop(to_drop, strict=strict).collect_schema().names()
+        assert names_out == ["b"]
diff --git a/tests/new_series_test.py b/tests/new_series_test.py
new file mode 100644
index 00000000..8ddcabd4
--- /dev/null
+++ b/tests/new_series_test.py
@@ -0,0 +1,36 @@
+from typing import Any
+
+import pandas as pd
+import pytest
+
+import narwhals.stable.v1 as nw
+from tests.utils import compare_dicts
+
+
+def test_new_series(constructor_eager: Any) -> None:
+    s = nw.from_native(constructor_eager({"a": [1, 2, 3]}), eager_only=True)["a"]
+    result = nw.new_series("b", [4, 1, 2], native_namespace=nw.get_native_namespace(s))
+    expected = {"b": [4, 1, 2]}
+    # all supported libraries auto-infer this to be int64, we can always special-case
+    # something different if necessary
+    assert result.dtype == nw.Int64
+    compare_dicts(result.to_frame(), expected)
+
+    result = nw.new_series(
+        "b", [4, 1, 2], nw.Int32, native_namespace=nw.get_native_namespace(s)
+    )
+    expected = {"b": [4, 1, 2]}
+    assert result.dtype == nw.Int32
+    compare_dicts(result.to_frame(), expected)
+
+
+def test_new_series_dask() -> None:
+    pytest.importorskip("dask")
+    pytest.importorskip("dask_expr", exc_type=ImportError)
+    import dask.dataframe as dd
+
+    df = nw.from_native(dd.from_pandas(pd.DataFrame({"a": [1, 2, 3]})))
+    with pytest.raises(
+        NotImplementedError, match="Dask support in Narwhals is lazy-only"
+    ):
+        nw.new_series("a", [1, 2, 3], native_namespace=nw.get_native_namespace(df))
diff --git a/tests/no_imports_test.py b/tests/no_imports_test.py
new file mode 100644
index 00000000..a89ed0ed
--- /dev/null
+++ b/tests/no_imports_test.py
@@ -0,0 +1,68 @@
+import sys
+
+import pandas as pd
+import polars as pl
+import pyarrow as pa
+import pytest
+
+import narwhals.stable.v1 as nw
+
+
+def test_polars(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.delitem(sys.modules, "pandas")
+    monkeypatch.delitem(sys.modules, "numpy")
+    monkeypatch.delitem(sys.modules, "pyarrow")
+    monkeypatch.delitem(sys.modules, "dask", raising=False)
+    df = pl.DataFrame({"a": [1, 1, 2], "b": [4, 5, 6]})
+    nw.from_native(df, eager_only=True).group_by("a").agg(nw.col("b").mean()).filter(
+        nw.col("a") > 1
+    )
+    assert "polars" in sys.modules
+    assert "pandas" not in sys.modules
+    assert "numpy" not in sys.modules
+    assert "pyarrow" not in sys.modules
+    assert "dask" not in sys.modules
+
+
+def test_pandas(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.delitem(sys.modules, "polars")
+    monkeypatch.delitem(sys.modules, "pyarrow")
+    monkeypatch.delitem(sys.modules, "dask", raising=False)
+    df = pd.DataFrame({"a": [1, 1, 2], "b": [4, 5, 6]})
+    nw.from_native(df, eager_only=True).group_by("a").agg(nw.col("b").mean()).filter(
+        nw.col("a") > 1
+    )
+    assert "polars" not in sys.modules
+    assert "pandas" in sys.modules
+    assert "numpy" in sys.modules
+    assert "pyarrow" not in sys.modules
+    assert "dask" not in sys.modules
+
+
+def test_dask(monkeypatch: pytest.MonkeyPatch) -> None:
+    pytest.importorskip("dask")
+    pytest.importorskip("dask_expr", exc_type=ImportError)
+    import dask.dataframe as dd
+
+    monkeypatch.delitem(sys.modules, "polars")
+    monkeypatch.delitem(sys.modules, "pyarrow")
+    df = dd.from_pandas(pd.DataFrame({"a": [1, 1, 2], "b": [4, 5, 6]}))
+    nw.from_native(df).group_by("a").agg(nw.col("b").mean()).filter(nw.col("a") > 1)
+    assert "polars" not in sys.modules
+    assert "pandas" in sys.modules
+    assert "numpy" in sys.modules
+    assert "pyarrow" not in sys.modules
+    assert "dask" in sys.modules
+
+
+def test_pyarrow(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.delitem(sys.modules, "polars")
+    monkeypatch.delitem(sys.modules, "pandas")
+    monkeypatch.delitem(sys.modules, "dask", raising=False)
+    df = pa.table({"a": [1, 2, 3], "b": [4, 5, 6]})
+    nw.from_native(df).group_by("a").agg(nw.col("b").mean()).filter(nw.col("a") > 1)
+    assert "polars" not in sys.modules
+    assert "pandas" not in sys.modules
+    assert "numpy" in sys.modules
+    assert "pyarrow" in sys.modules
+    assert "dask" not in sys.modules
diff --git a/tests/series_only/arrow_c_stream_test.py b/tests/series_only/arrow_c_stream_test.py
new file mode 100644
index 00000000..9964d740
--- /dev/null
+++ b/tests/series_only/arrow_c_stream_test.py
@@ -0,0 +1,41 @@
+import polars as pl
+import pyarrow as pa
+import pyarrow.compute as pc
+import pytest
+
+import narwhals.stable.v1 as nw
+from narwhals.utils import parse_version
+
+
+@pytest.mark.skipif(
+    parse_version(pl.__version__) < (1, 3), reason="too old for pycapsule in Polars"
+)
+def test_arrow_c_stream_test() -> None:
+    s = nw.from_native(pl.Series([1, 2, 3]), series_only=True)
+    result = pa.chunked_array(s)
+    expected = pa.chunked_array([[1, 2, 3]])
+    assert pc.all(pc.equal(result, expected)).as_py()
+
+
+@pytest.mark.skipif(
+    parse_version(pl.__version__) < (1, 3), reason="too old for pycapsule in Polars"
+)
+def test_arrow_c_stream_test_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
+    # "poison" the dunder method to make sure it actually got called above
+    monkeypatch.setattr("narwhals.series.Series.__arrow_c_stream__", lambda *_: 1 / 0)
+    s = nw.from_native(pl.Series([1, 2, 3]), series_only=True)
+    with pytest.raises(ZeroDivisionError, match="division by zero"):
+        pa.chunked_array(s)
+
+
+@pytest.mark.skipif(
+    parse_version(pl.__version__) < (1, 3), reason="too old for pycapsule in Polars"
+)
+def test_arrow_c_stream_test_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
+    # Check that fallback to PyArrow works
+    monkeypatch.delattr("polars.Series.__arrow_c_stream__")
+    s = nw.from_native(pl.Series([1, 2, 3]).to_frame("a"), eager_only=True)["a"]
+    s.__arrow_c_stream__()
+    result = pa.chunked_array(s)
+    expected = pa.chunked_array([[1, 2, 3]])
+    assert pc.all(pc.equal(result, expected)).as_py()