Repository: narwhals-dev/narwhals
Pull request #1308 (2024-11-02 14:49:38)

depr: deprecate `strict` in `from_native` / `to_native` in favour of `pass_through`
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [ ] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue # 
- Closes #

## Checklist

- [ ] Code follows style guide (ruff)
- [ ] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.




Patch:
diff --git a/docs/backcompat.md b/docs/backcompat.md
index 2090eae7..807d51ed 100644
--- a/docs/backcompat.md
+++ b/docs/backcompat.md
@@ -99,8 +99,27 @@ before making any change.
 
 ### After `stable.v1`
 
-- `Datetime` and `Duration` dtypes hash using both `time_unit` and `time_zone`.
-    The effect of this can be seen when doing `dtype in {...}` checks:
+- Since Narwhals 1.13.0, the `strict` parameter in `from_native`, `to_native`, and `narwhalify`
+    has been deprecated in favour of `pass_through`. This is because several users expressed
+    confusion/surprise over what `strict=False` did.
+    ```python
+    # v1 syntax:
+    nw.from_native(df, strict=False)
+
+    # main namespace (and, when we get there, v2) syntax:
+    nw.from_native(df, pass_through=True)
+    ```
+    If you are using Narwhals>=1.13.0, then we recommend using `pass_through`, as that
+    works consistently across namespaces.
+
+    In the future:
+
+    - in the main Narwhals namespace, `strict` will be removed in favour of `pass_through`
+    - in `stable.v1`, we will keep both `strict` and `pass_through`
+
+- Since Narwhals 1.9.0, `Datetime` and `Duration` dtypes hash using both `time_unit` and
+    `time_zone`.
+    The effect of this can be seen when placing these dtypes in sets:
 
     ```python exec="1" source="above" session="backcompat"
     import narwhals.stable.v1 as nw_v1
diff --git a/narwhals/dataframe.py b/narwhals/dataframe.py
index 3ddaa281..bf8bb5c9 100644
--- a/narwhals/dataframe.py
+++ b/narwhals/dataframe.py
@@ -2874,7 +2874,7 @@ class LazyFrame(BaseFrame[FrameT]):
             └─────┴─────┴─────┘
         """
 
-        return to_native(narwhals_object=self, strict=True)
+        return to_native(narwhals_object=self, pass_through=False)
 
     # inherited
     def pipe(self, function: Callable[[Any], Self], *args: Any, **kwargs: Any) -> Self:
diff --git a/narwhals/functions.py b/narwhals/functions.py
index b8dfffbe..20efa7b4 100644
--- a/narwhals/functions.py
+++ b/narwhals/functions.py
@@ -361,7 +361,7 @@ def _from_dict_impl(
         else:
             msg = "Calling `from_dict` without `native_namespace` is only supported if all input values are already Narwhals Series"
             raise TypeError(msg)
-        data = {key: to_native(value, strict=False) for key, value in data.items()}
+        data = {key: to_native(value, pass_through=True) for key, value in data.items()}
     implementation = Implementation.from_native_namespace(native_namespace)
 
     if implementation is Implementation.POLARS:
diff --git a/narwhals/stable/v1/__init__.py b/narwhals/stable/v1/__init__.py
index 5bc762dd..20f6fee3 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -63,6 +63,7 @@ from narwhals.utils import maybe_convert_dtypes
 from narwhals.utils import maybe_get_index
 from narwhals.utils import maybe_reset_index
 from narwhals.utils import maybe_set_index
+from narwhals.utils import validate_strict_and_pass_though
 
 if TYPE_CHECKING:
     from types import ModuleType
@@ -775,12 +776,212 @@ def from_native(
     """
 
 
+@overload
+def from_native(
+    native_object: IntoDataFrameT | IntoSeriesT,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: Literal[True],
+    series_only: None = ...,
+    allow_series: Literal[True],
+) -> DataFrame[IntoDataFrameT]: ...
+
+
+@overload
+def from_native(
+    native_object: IntoDataFrameT | IntoSeriesT,
+    *,
+    pass_through: Literal[True],
+    eager_only: Literal[True],
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: Literal[True],
+) -> DataFrame[IntoDataFrameT] | Series: ...
+
+
+@overload
+def from_native(
+    native_object: IntoDataFrameT,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: Literal[True],
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> DataFrame[IntoDataFrameT]: ...
+
+
+@overload
+def from_native(
+    native_object: T,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: Literal[True],
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> T: ...
+
+
+@overload
+def from_native(
+    native_object: IntoDataFrameT,
+    *,
+    pass_through: Literal[True],
+    eager_only: Literal[True],
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> DataFrame[IntoDataFrameT]: ...
+
+
+@overload
+def from_native(
+    native_object: T,
+    *,
+    pass_through: Literal[True],
+    eager_only: Literal[True],
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> T: ...
+
+
+@overload
+def from_native(
+    native_object: IntoFrameT | IntoSeriesT,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: Literal[True],
+) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series: ...
+
+
+@overload
+def from_native(
+    native_object: IntoSeriesT,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: Literal[True],
+    allow_series: None = ...,
+) -> Series: ...
+
+
+@overload
+def from_native(
+    native_object: IntoFrameT,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]: ...
+
+
+@overload
+def from_native(
+    native_object: T,
+    *,
+    pass_through: Literal[True],
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> T: ...
+
+
+@overload
+def from_native(
+    native_object: IntoDataFrameT,
+    *,
+    pass_through: Literal[False] = ...,
+    eager_only: None = ...,
+    eager_or_interchange_only: Literal[True],
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> DataFrame[IntoDataFrameT]:
+    """
+    from_native(df, pass_through=False, eager_or_interchange_only=True)
+    from_native(df, eager_or_interchange_only=True)
+    """
+
+
+@overload
+def from_native(
+    native_object: IntoDataFrameT,
+    *,
+    pass_through: Literal[False] = ...,
+    eager_only: Literal[True],
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> DataFrame[IntoDataFrameT]:
+    """
+    from_native(df, pass_through=False, eager_only=True)
+    from_native(df, eager_only=True)
+    """
+
+
+@overload
+def from_native(
+    native_object: IntoFrameT | IntoSeriesT,
+    *,
+    pass_through: Literal[False] = ...,
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: Literal[True],
+) -> DataFrame[Any] | LazyFrame[Any] | Series:
+    """
+    from_native(df, pass_through=False, allow_series=True)
+    from_native(df, allow_series=True)
+    """
+
+
+@overload
+def from_native(
+    native_object: IntoSeriesT,
+    *,
+    pass_through: Literal[False] = ...,
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: Literal[True],
+    allow_series: None = ...,
+) -> Series:
+    """
+    from_native(df, pass_through=False, series_only=True)
+    from_native(df, series_only=True)
+    """
+
+
+@overload
+def from_native(
+    native_object: IntoFrameT,
+    *,
+    pass_through: Literal[False] = ...,
+    eager_only: None = ...,
+    eager_or_interchange_only: None = ...,
+    series_only: None = ...,
+    allow_series: None = ...,
+) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]:
+    """
+    from_native(df, pass_through=False)
+    from_native(df)
+    """
+
+
 # All params passed in as variables
 @overload
 def from_native(
     native_object: Any,
     *,
-    strict: bool,
+    pass_through: bool,
     eager_only: bool | None,
     eager_or_interchange_only: bool | None = None,
     series_only: bool | None,
@@ -791,7 +992,8 @@ def from_native(
 def from_native(
     native_object: Any,
     *,
-    strict: bool = True,
+    strict: bool | None = None,
+    pass_through: bool | None = None,
     eager_only: bool | None = None,
     eager_or_interchange_only: bool | None = None,
     series_only: bool | None = None,
@@ -811,8 +1013,19 @@ def from_native(
             - pandas.Series
             - polars.Series
             - anything with a `__narwhals_series__` method
-        strict: Whether to raise if object can't be converted (default) or
-            to just leave it as-is.
+        strict: Determine what happens if the object isn't supported by Narwhals:
+
+            - `True` (default): raise an error
+            - `False`: pass object through as-is
+
+            **Deprecated** (v1.13.0):
+                Please use `pass_through` instead. Note that `strict` is still available
+                (and won't emit a deprecation warning) if you use `narwhals.stable.v1`,
+                see [perfect backwards compatibility policy](https://narwhals-dev.github.io/narwhals/backcompat/).
+        pass_through: Determine what happens if the object isn't supported by Narwhals:
+
+            - `False` (default): raise an error
+            - `True`: pass object through as-is
         eager_only: Whether to only allow eager objects.
         eager_or_interchange_only: Whether to only allow eager objects or objects which
             implement the Dataframe Interchange Protocol.
@@ -829,9 +1042,14 @@ def from_native(
         return native_object
     if isinstance(native_object, Series) and (series_only or allow_series):
         return native_object
+
+    pass_through = validate_strict_and_pass_though(
+        strict, pass_through, pass_through_default=False, emit_deprecation_warning=False
+    )
+
     result = _from_native_impl(
         native_object,
-        strict=strict,
+        pass_through=pass_through,
         eager_only=eager_only,
         eager_or_interchange_only=eager_or_interchange_only,
         series_only=series_only,
@@ -844,7 +1062,8 @@ def from_native(
 def narwhalify(
     func: Callable[..., Any] | None = None,
     *,
-    strict: bool = False,
+    strict: bool | None = None,
+    pass_through: bool | None = None,
     eager_only: bool | None = False,
     eager_or_interchange_only: bool | None = False,
     series_only: bool | None = False,
@@ -905,13 +1124,17 @@ def narwhalify(
         allow_series: Whether to allow series (default is only dataframe / lazyframe).
     """
 
+    pass_through = validate_strict_and_pass_though(
+        strict, pass_through, pass_through_default=True, emit_deprecation_warning=False
+    )
+
     def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
         @wraps(func)
         def wrapper(*args: Any, **kwargs: Any) -> Any:
             args = [
                 from_native(
                     arg,
-                    strict=strict,
+                    pass_through=pass_through,
                     eager_only=eager_only,
                     eager_or_interchange_only=eager_or_interchange_only,
                     series_only=series_only,
@@ -923,7 +1146,7 @@ def narwhalify(
             kwargs = {
                 name: from_native(
                     value,
-                    strict=strict,
+                    pass_through=pass_through,
                     eager_only=eager_only,
                     eager_or_interchange_only=eager_or_interchange_only,
                     series_only=series_only,
@@ -944,7 +1167,7 @@ def narwhalify(
 
             result = func(*args, **kwargs)
 
-            return to_native(result, strict=strict)
+            return to_native(result, pass_through=pass_through)
 
         return wrapper
 
diff --git a/narwhals/translate.py b/narwhals/translate.py
index 65cc4422..129fe671 100644
--- a/narwhals/translate.py
+++ b/narwhals/translate.py
@@ -57,22 +57,23 @@ NON_TEMPORAL_SCALAR_TYPES = (
 
 @overload
 def to_native(
-    narwhals_object: DataFrame[IntoDataFrameT], *, strict: Literal[True] = ...
+    narwhals_object: DataFrame[IntoDataFrameT], *, pass_through: Literal[False] = ...
 ) -> IntoDataFrameT: ...
 @overload
 def to_native(
-    narwhals_object: LazyFrame[IntoFrameT], *, strict: Literal[True] = ...
+    narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ...
 ) -> IntoFrameT: ...
 @overload
-def to_native(narwhals_object: Series, *, strict: Literal[True] = ...) -> Any: ...
+def to_native(narwhals_object: Series, *, pass_through: Literal[False] = ...) -> Any: ...
 @overload
-def to_native(narwhals_object: Any, *, strict: bool) -> Any: ...
+def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ...
 
 
 def to_native(
     narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series,
     *,
-    strict: bool = True,
+    strict: bool | None = None,
+    pass_through: bool | None = None,
 ) -> IntoFrameT | Any:
     """
     Convert Narwhals object to native one.
@@ -86,13 +87,18 @@ def to_native(
     """
     from narwhals.dataframe import BaseFrame
     from narwhals.series import Series
+    from narwhals.utils import validate_strict_and_pass_though
+
+    pass_through = validate_strict_and_pass_though(
+        strict, pass_through, pass_through_default=False, emit_deprecation_warning=True
+    )
 
     if isinstance(narwhals_object, BaseFrame):
         return narwhals_object._compliant_frame._native_frame
     if isinstance(narwhals_object, Series):
         return narwhals_object._compliant_series._native_series
 
-    if strict:
+    if not pass_through:
         msg = f"Expected Narwhals object, got {type(narwhals_object)}."
         raise TypeError(msg)
     return narwhals_object
@@ -102,7 +108,7 @@ def to_native(
 def from_native(
     native_object: IntoDataFrameT | IntoSeriesT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: Literal[True],
     series_only: None = ...,
@@ -114,7 +120,7 @@ def from_native(
 def from_native(
     native_object: IntoDataFrameT | IntoSeriesT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: Literal[True],
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
@@ -126,7 +132,7 @@ def from_native(
 def from_native(
     native_object: IntoDataFrameT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: Literal[True],
     series_only: None = ...,
@@ -138,7 +144,7 @@ def from_native(
 def from_native(
     native_object: T,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: Literal[True],
     series_only: None = ...,
@@ -150,7 +156,7 @@ def from_native(
 def from_native(
     native_object: IntoDataFrameT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: Literal[True],
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
@@ -162,7 +168,7 @@ def from_native(
 def from_native(
     native_object: T,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: Literal[True],
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
@@ -174,7 +180,7 @@ def from_native(
 def from_native(
     native_object: IntoFrameT | IntoSeriesT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
@@ -186,7 +192,7 @@ def from_native(
 def from_native(
     native_object: IntoSeriesT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: Literal[True],
@@ -198,7 +204,7 @@ def from_native(
 def from_native(
     native_object: IntoFrameT,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
@@ -210,7 +216,7 @@ def from_native(
 def from_native(
     native_object: T,
     *,
-    strict: Literal[False],
+    pass_through: Literal[True],
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
@@ -222,14 +228,14 @@ def from_native(
 def from_native(
     native_object: IntoDataFrameT,
     *,
-    strict: Literal[True] = ...,
+    pass_through: Literal[False] = ...,
     eager_only: None = ...,
     eager_or_interchange_only: Literal[True],
     series_only: None = ...,
     allow_series: None = ...,
 ) -> DataFrame[IntoDataFrameT]:
     """
-    from_native(df, strict=True, eager_or_interchange_only=True)
+    from_native(df, pass_through=False, eager_or_interchange_only=True)
     from_native(df, eager_or_interchange_only=True)
     """
 
@@ -238,14 +244,14 @@ def from_native(
 def from_native(
     native_object: IntoDataFrameT,
     *,
-    strict: Literal[True] = ...,
+    pass_through: Literal[False] = ...,
     eager_only: Literal[True],
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
     allow_series: None = ...,
 ) -> DataFrame[IntoDataFrameT]:
     """
-    from_native(df, strict=True, eager_only=True)
+    from_native(df, pass_through=False, eager_only=True)
     from_native(df, eager_only=True)
     """
 
@@ -254,14 +260,14 @@ def from_native(
 def from_native(
     native_object: IntoFrameT | IntoSeriesT,
     *,
-    strict: Literal[True] = ...,
+    pass_through: Literal[False] = ...,
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
     allow_series: Literal[True],
 ) -> DataFrame[Any] | LazyFrame[Any] | Series:
     """
-    from_native(df, strict=True, allow_series=True)
+    from_native(df, pass_through=False, allow_series=True)
     from_native(df, allow_series=True)
     """
 
@@ -270,14 +276,14 @@ def from_native(
 def from_native(
     native_object: IntoSeriesT,
     *,
-    strict: Literal[True] = ...,
+    pass_through: Literal[False] = ...,
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: Literal[True],
     allow_series: None = ...,
 ) -> Series:
     """
-    from_native(df, strict=True, series_only=True)
+    from_native(df, pass_through=False, series_only=True)
     from_native(df, series_only=True)
     """
 
@@ -286,14 +292,14 @@ def from_native(
 def from_native(
     native_object: IntoFrameT,
     *,
-    strict: Literal[True] = ...,
+    pass_through: Literal[False] = ...,
     eager_only: None = ...,
     eager_or_interchange_only: None = ...,
     series_only: None = ...,
     allow_series: None = ...,
 ) -> DataFrame[IntoFrameT] | LazyFrame[IntoFrameT]:
     """
-    from_native(df, strict=True)
+    from_native(df, pass_through=False)
     from_native(df)
     """
 
@@ -303,7 +309,7 @@ def from_native(
 def from_native(
     native_object: Any,
     *,
-    strict: bool,
+    pass_through: bool,
     eager_only: bool | None,
     eager_or_interchange_only: bool | None = None,
     series_only: bool | None,
@@ -314,7 +320,8 @@ def from_native(
 def from_native(
     native_object: Any,
     *,
-    strict: bool = True,
+    strict: bool | None = None,
+    pass_through: bool | None = None,
     eager_only: bool | None = None,
     eager_or_interchange_only: bool | None = None,
     series_only: bool | None = None,
@@ -334,8 +341,19 @@ def from_native(
             - pandas.Series
             - polars.Series
             - anything with a `__narwhals_series__` method
-        strict: Whether to raise if object can't be converted (default) or
-            to just leave it as-is.
+        strict: Determine what happens if the object isn't supported by Narwhals:
+
+            - `True` (default): raise an error
+            - `False`: pass object through as-is
+
+            **Deprecated** (v1.13.0):
+                Please use `pass_through` instead. Note that `strict` is still available
+                (and won't emit a deprecation warning) if you use `narwhals.stable.v1`,
+                see [perfect backwards compatibility policy](https://narwhals-dev.github.io/narwhals/backcompat/).
+        pass_through: Determine what happens if the object isn't supported by Narwhals:
+
+            - `False` (default): raise an error
+            - `True`: pass object through as-is
         eager_only: Whether to only allow eager objects.
         eager_or_interchange_only: Whether to only allow eager objects or objects which
             implement the Dataframe Interchange Protocol.
@@ -346,10 +364,15 @@ def from_native(
         narwhals.DataFrame or narwhals.LazyFrame or narwhals.Series
     """
     from narwhals import dtypes
+    from narwhals.utils import validate_strict_and_pass_though
+
+    pass_through = validate_strict_and_pass_though(
+        strict, pass_through, pass_through_default=False, emit_deprecation_warning=True
+    )
 
     return _from_native_impl(
         native_object,
-        strict=strict,
+        pass_through=pass_through,
         eager_only=eager_only,
         eager_or_interchange_only=eager_or_interchange_only,
         series_only=series_only,
@@ -361,7 +384,7 @@ def from_native(
 def _from_native_impl(  # noqa: PLR0915
     native_object: Any,
     *,
-    strict: bool = True,
+    pass_through: bool = False,
     eager_only: bool | None = None,
     eager_or_interchange_only: bool | None = None,
     series_only: bool | None = None,
@@ -403,7 +426,7 @@ def _from_native_impl(  # noqa: PLR0915
     # Extensions
     if hasattr(native_object, "__narwhals_dataframe__"):
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with dataframe"
                 raise TypeError(msg)
             return native_object
@@ -413,12 +436,12 @@ def _from_native_impl(  # noqa: PLR0915
         )
     elif hasattr(native_object, "__narwhals_lazyframe__"):
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with lazyframe"
                 raise TypeError(msg)
             return native_object
         if eager_only or eager_or_interchange_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with lazyframe"
                 raise TypeError(msg)
             return native_object
@@ -428,7 +451,7 @@ def _from_native_impl(  # noqa: PLR0915
         )
     elif hasattr(native_object, "__narwhals_series__"):
         if not allow_series:
-            if strict:
+            if not pass_through:
                 msg = "Please set `allow_series=True` or `series_only=True`"
                 raise TypeError(msg)
             return native_object
@@ -440,7 +463,7 @@ def _from_native_impl(  # noqa: PLR0915
     # Polars
     elif is_polars_dataframe(native_object):
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with polars.DataFrame"
                 raise TypeError(msg)
             return native_object
@@ -455,12 +478,12 @@ def _from_native_impl(  # noqa: PLR0915
         )
     elif is_polars_lazyframe(native_object):
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with polars.LazyFrame"
                 raise TypeError(msg)
             return native_object
         if eager_only or eager_or_interchange_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with polars.LazyFrame"
                 raise TypeError(msg)
             return native_object
@@ -476,7 +499,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_polars_series(native_object):
         pl = get_polars()
         if not allow_series:
-            if strict:
+            if not pass_through:
                 msg = "Please set `allow_series=True` or `series_only=True`"
                 raise TypeError(msg)
             return native_object
@@ -492,7 +515,7 @@ def _from_native_impl(  # noqa: PLR0915
     # pandas
     elif is_pandas_dataframe(native_object):
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with dataframe"
                 raise TypeError(msg)
             return native_object
@@ -508,7 +531,7 @@ def _from_native_impl(  # noqa: PLR0915
         )
     elif is_pandas_series(native_object):
         if not allow_series:
-            if strict:
+            if not pass_through:
                 msg = "Please set `allow_series=True` or `series_only=True`"
                 raise TypeError(msg)
             return native_object
@@ -527,7 +550,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_modin_dataframe(native_object):  # pragma: no cover
         mpd = get_modin()
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with modin.DataFrame"
                 raise TypeError(msg)
             return native_object
@@ -543,7 +566,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_modin_series(native_object):  # pragma: no cover
         mpd = get_modin()
         if not allow_series:
-            if strict:
+            if not pass_through:
                 msg = "Please set `allow_series=True` or `series_only=True`"
                 raise TypeError(msg)
             return native_object
@@ -561,7 +584,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_cudf_dataframe(native_object):  # pragma: no cover
         cudf = get_cudf()
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with cudf.DataFrame"
                 raise TypeError(msg)
             return native_object
@@ -577,7 +600,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_cudf_series(native_object):  # pragma: no cover
         cudf = get_cudf()
         if not allow_series:
-            if strict:
+            if not pass_through:
                 msg = "Please set `allow_series=True` or `series_only=True`"
                 raise TypeError(msg)
             return native_object
@@ -595,7 +618,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_pyarrow_table(native_object):
         pa = get_pyarrow()
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with arrow table"
                 raise TypeError(msg)
             return native_object
@@ -610,7 +633,7 @@ def _from_native_impl(  # noqa: PLR0915
     elif is_pyarrow_chunked_array(native_object):
         pa = get_pyarrow()
         if not allow_series:
-            if strict:
+            if not pass_through:
                 msg = "Please set `allow_series=True` or `series_only=True`"
                 raise TypeError(msg)
             return native_object
@@ -627,12 +650,12 @@ def _from_native_impl(  # noqa: PLR0915
     # Dask
     elif is_dask_dataframe(native_object):
         if series_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `series_only` with dask DataFrame"
                 raise TypeError(msg)
             return native_object
         if eager_only or eager_or_interchange_only:
-            if strict:
+            if not pass_through:
                 msg = "Cannot only use `eager_only` or `eager_or_interchange_only` with dask DataFrame"
                 raise TypeError(msg)
             return native_object
@@ -651,7 +674,7 @@ def _from_native_impl(  # noqa: PLR0915
     # DuckDB
     elif is_duckdb_relation(native_object):
         if eager_only or series_only:  # pragma: no cover
-            if strict:
+            if not pass_through:
                 msg = (
                     "Cannot only use `series_only=True` or `eager_only=False` "
                     "with DuckDB Relation"
@@ -667,7 +690,7 @@ def _from_native_impl(  # noqa: PLR0915
     # Ibis
     elif is_ibis_table(native_object):  # pragma: no cover
         if eager_only or series_only:
-            if strict:
+            if not pass_through:
                 msg = (
                     "Cannot only use `series_only=True` or `eager_only=False` "
                     "with Ibis table"
@@ -682,7 +705,7 @@ def _from_native_impl(  # noqa: PLR0915
     # Interchange protocol
     elif hasattr(native_object, "__dataframe__"):
         if eager_only or series_only:
-            if strict:
+            if not pass_through:
                 msg = (
                     "Cannot only use `series_only=True` or `eager_only=False` "
                     "with object which only implements __dataframe__"
@@ -694,7 +717,7 @@ def _from_native_impl(  # noqa: PLR0915
             level="interchange",
         )
 
-    elif strict:
+    elif not pass_through:
         msg = f"Expected pandas-like dataframe, Polars dataframe, or Polars lazyframe, got: {type(native_object)}"
         raise TypeError(msg)
     return native_object
@@ -721,7 +744,8 @@ def get_native_namespace(obj: Any) -> Any:
 def narwhalify(
     func: Callable[..., Any] | None = None,
     *,
-    strict: bool = False,
+    strict: bool | None = None,
+    pass_through: bool | None = None,
     eager_only: bool | None = False,
     eager_or_interchange_only: bool | None = False,
     series_only: bool | None = False,
@@ -781,6 +805,11 @@ def narwhalify(
         series_only: Whether to only allow series.
         allow_series: Whether to allow series (default is only dataframe / lazyframe).
     """
+    from narwhals.utils import validate_strict_and_pass_though
+
+    pass_through = validate_strict_and_pass_though(
+        strict, pass_through, pass_through_default=True, emit_deprecation_warning=True
+    )
 
     def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
         @wraps(func)
@@ -788,7 +817,7 @@ def narwhalify(
             args = [
                 from_native(
                     arg,
-                    strict=strict,
+                    pass_through=pass_through,
                     eager_only=eager_only,
                     eager_or_interchange_only=eager_or_interchange_only,
                     series_only=series_only,
@@ -800,7 +829,7 @@ def narwhalify(
             kwargs = {
                 name: from_native(
                     value,
-                    strict=strict,
+                    pass_through=pass_through,
                     eager_only=eager_only,
                     eager_or_interchange_only=eager_or_interchange_only,
                     series_only=series_only,
@@ -821,7 +850,7 @@ def narwhalify(
 
             result = func(*args, **kwargs)
 
-            return to_native(result, strict=strict)
+            return to_native(result, pass_through=pass_through)
 
         return wrapper
 
diff --git a/narwhals/utils.py b/narwhals/utils.py
index 66c2bade..496dee42 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -549,3 +549,83 @@ def parse_columns_to_drop(
 
 def is_sequence_but_not_str(sequence: Any) -> TypeGuard[Sequence[Any]]:
     return isinstance(sequence, Sequence) and not isinstance(sequence, str)
+
+
+def find_stacklevel() -> int:
+    """
+    Find the first place in the stack that is not inside narwhals.
+
+    Taken from:
+    https://github.com/pandas-dev/pandas/blob/ab89c53f48df67709a533b6a95ce3d911871a0a8/pandas/util/_exceptions.py#L30-L51
+    """
+    import inspect
+    from pathlib import Path
+
+    import narwhals as nw
+
+    pkg_dir = str(Path(nw.__file__).parent)
+
+    # https://stackoverflow.com/questions/17407119/python-inspect-stack-is-slow
+    frame = inspect.currentframe()
+    n = 0
+    try:
+        while frame:
+            fname = inspect.getfile(frame)
+            if fname.startswith(pkg_dir) or (
+                (qualname := getattr(frame.f_code, "co_qualname", None))
+                # ignore @singledispatch wrappers
+                and qualname.startswith("singledispatch.")
+            ):
+                frame = frame.f_back
+                n += 1
+            else:  # pragma: no cover
+                break
+        else:  # pragma: no cover
+            pass
+    finally:
+        # https://docs.python.org/3/library/inspect.html
+        # > Though the cycle detector will catch these, destruction of the frames
+        # > (and local variables) can be made deterministic by removing the cycle
+        # > in a finally clause.
+        del frame
+    return n
+
+
+def issue_deprecation_warning(message: str, _version: str) -> None:
+    """
+    Issue a deprecation warning.
+
+    Parameters
+    ----------
+    message
+        The message associated with the warning.
+    version
+        Narwhals version when the warning was introduced. Just used for internal
+        bookkeeping.
+    """
+    warn(message=message, category=DeprecationWarning, stacklevel=find_stacklevel())
+
+
+def validate_strict_and_pass_though(
+    strict: bool | None,
+    pass_through: bool | None,
+    *,
+    pass_through_default: bool,
+    emit_deprecation_warning: bool,
+) -> bool:
+    if strict is None and pass_through is None:
+        pass_through = pass_through_default
+    elif strict is not None and pass_through is None:
+        if emit_deprecation_warning:
+            msg = (
+                "`strict` in `from_native` is deprecated, please use `pass_through` instead.\n\n"
+                "Note: `strict` will remain available in `narwhals.stable.v1`."
+            )
+            issue_deprecation_warning(msg, _version="1.13.0")
+        pass_through = not strict
+    elif strict is None and pass_through is not None:
+        pass
+    else:
+        msg = "Cannot pass both `strict` and `pass_through`"
+        raise ValueError(msg)
+    return pass_through


Test patch:
diff --git a/tests/translate/from_native_test.py b/tests/translate/from_native_test.py
index e1fc4a59..05c92250 100644
--- a/tests/translate/from_native_test.py
+++ b/tests/translate/from_native_test.py
@@ -226,9 +226,15 @@ def test_from_native_strict_false_typing() -> None:
     nw.from_native(df, strict=False, eager_only=True)
     nw.from_native(df, strict=False, eager_or_interchange_only=True)
 
-    unstable_nw.from_native(df, strict=False)
-    unstable_nw.from_native(df, strict=False, eager_only=True)
-    unstable_nw.from_native(df, strict=False, eager_or_interchange_only=True)
+    with pytest.deprecated_call(match="please use `pass_through` instead"):
+        unstable_nw.from_native(df, strict=False)  # type: ignore[call-overload]
+        unstable_nw.from_native(df, strict=False, eager_only=True)  # type: ignore[call-overload]
+        unstable_nw.from_native(df, strict=False, eager_or_interchange_only=True)  # type: ignore[call-overload]
+
+
+def test_from_native_strict_false_invalid() -> None:
+    with pytest.raises(ValueError, match="Cannot pass both `strict`"):
+        nw.from_native({"a": [1, 2, 3]}, strict=True, pass_through=False)  # type: ignore[call-overload]
 
 
 def test_from_mock_interchange_protocol_non_strict() -> None:
diff --git a/tests/translate/narwhalify_test.py b/tests/translate/narwhalify_test.py
index 5c700a19..4dc34654 100644
--- a/tests/translate/narwhalify_test.py
+++ b/tests/translate/narwhalify_test.py
@@ -61,21 +61,23 @@ def test_narwhalify_method_called() -> None:
 
 
 def test_narwhalify_method_invalid() -> None:
-    class Foo:
-        @nw.narwhalify(strict=True, eager_only=True)
-        def func(self) -> Foo:  # pragma: no cover
-            return self
+    with pytest.deprecated_call(match="please use `pass_through` instead"):
 
-        @nw.narwhalify(strict=True, eager_only=True)
-        def fun2(self, df: Any) -> Any:  # pragma: no cover
-            return df
+        class Foo:
+            @nw.narwhalify(strict=True, eager_only=True)
+            def func(self) -> Foo:  # pragma: no cover
+                return self
 
-    with pytest.raises(TypeError):
-        Foo().func()
+            @nw.narwhalify(strict=True, eager_only=True)
+            def fun2(self, df: Any) -> Any:  # pragma: no cover
+                return df
+
+        with pytest.raises(TypeError):
+            Foo().func()
 
 
 def test_narwhalify_invalid() -> None:
-    @nw.narwhalify(strict=True)
+    @nw.narwhalify(pass_through=False)
     def func() -> None:  # pragma: no cover
         return None
 
diff --git a/tests/translate/to_native_test.py b/tests/translate/to_native_test.py
index 3d116a45..e832e73e 100644
--- a/tests/translate/to_native_test.py
+++ b/tests/translate/to_native_test.py
@@ -13,27 +13,27 @@ if TYPE_CHECKING:
 
 
 @pytest.mark.parametrize(
-    ("method", "strict", "context"),
+    ("method", "pass_through", "context"),
     [
-        ("head", True, does_not_raise()),
         ("head", False, does_not_raise()),
-        ("to_numpy", False, does_not_raise()),
+        ("head", True, does_not_raise()),
+        ("to_numpy", True, does_not_raise()),
         (
             "to_numpy",
-            True,
+            False,
             pytest.raises(TypeError, match="Expected Narwhals object, got"),
         ),
     ],
 )
 def test_to_native(
-    constructor_eager: ConstructorEager, method: str, strict: Any, context: Any
+    constructor_eager: ConstructorEager, method: str, *, pass_through: bool, context: Any
 ) -> None:
     df = nw.from_native(constructor_eager({"a": [1, 2, 3]}))
 
     with context:
-        nw.to_native(getattr(df, method)(), strict=strict)
+        nw.to_native(getattr(df, method)(), pass_through=pass_through)
 
     s = df["a"]
 
     with context:
-        nw.to_native(getattr(s, method)(), strict=strict)
+        nw.to_native(getattr(s, method)(), pass_through=pass_through)