Repository: narwhals-dev/narwhals
Pull request #1312 (2024-11-03 11:23:46)

 Revert "Revert "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.



MarcoGorelli: Downstream, Altair still works when checked manually too. I'll do another few manual checks before releasing, we need to be careful here

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..e11cc687 100644
--- a/narwhals/stable/v1/__init__.py
+++ b/narwhals/stable/v1/__init__.py
@@ -51,7 +51,6 @@ from narwhals.stable.v1.dtypes import UInt64
 from narwhals.stable.v1.dtypes import Unknown
 from narwhals.translate import _from_native_impl
 from narwhals.translate import get_native_namespace
-from narwhals.translate import to_native
 from narwhals.translate import to_py_scalar
 from narwhals.typing import IntoDataFrameT
 from narwhals.typing import IntoFrameT
@@ -63,6 +62,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 +775,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 +991,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 +1012,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 +1041,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,
@@ -841,10 +1058,72 @@ def from_native(
     return _stableify(result)
 
 
+@overload
+def to_native(
+    narwhals_object: DataFrame[IntoDataFrameT], *, strict: Literal[True] = ...
+) -> IntoDataFrameT: ...
+@overload
+def to_native(
+    narwhals_object: LazyFrame[IntoFrameT], *, strict: Literal[True] = ...
+) -> IntoFrameT: ...
+@overload
+def to_native(narwhals_object: Series, *, strict: Literal[True] = ...) -> Any: ...
+@overload
+def to_native(narwhals_object: Any, *, strict: bool) -> Any: ...
+@overload
+def to_native(
+    narwhals_object: DataFrame[IntoDataFrameT], *, pass_through: Literal[False] = ...
+) -> IntoDataFrameT: ...
+@overload
+def to_native(
+    narwhals_object: LazyFrame[IntoFrameT], *, pass_through: Literal[False] = ...
+) -> IntoFrameT: ...
+@overload
+def to_native(narwhals_object: Series, *, pass_through: Literal[False] = ...) -> Any: ...
+@overload
+def to_native(narwhals_object: Any, *, pass_through: bool) -> Any: ...
+
+
+def to_native(
+    narwhals_object: DataFrame[IntoFrameT] | LazyFrame[IntoFrameT] | Series,
+    *,
+    strict: bool | None = None,
+    pass_through: bool | None = None,
+) -> IntoFrameT | Any:
+    """
+    Convert Narwhals object to native one.
+
+    Arguments:
+        narwhals_object: Narwhals object.
+        strict: whether to raise on non-Narwhals input.
+
+    Returns:
+        Object of class that user started with.
+    """
+    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=False
+    )
+
+    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 not pass_through:
+        msg = f"Expected Narwhals object, got {type(narwhals_object)}."
+        raise TypeError(msg)
+    return narwhals_object
+
+
 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 +1184,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 +1206,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 +1227,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..e538bee1 100644
--- a/narwhals/utils.py
+++ b/narwhals/utils.py
@@ -549,3 +549,84 @@ 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`.\n"
+                "See https://narwhals-dev.github.io/narwhals/backcompat/ for more information.\n"
+            )
+            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/.github/workflows/downstream_tests.yml b/.github/workflows/downstream_tests.yml
index 812405fc..95b4d14e 100644
--- a/.github/workflows/downstream_tests.yml
+++ b/.github/workflows/downstream_tests.yml
@@ -1,8 +1,9 @@
 name: Test Downstream Libraries
 
 on: 
-  workflow_call:
-  workflow_dispatch:
+  pull_request:
+  push:
+    branches: [main]
 
 jobs:
   altair:
@@ -230,59 +231,59 @@ jobs:
             cd tubular
             pytest tests --config-file=pyproject.toml
   
-  vegafusion:
-     env:
-       UV_SYSTEM_PYTHON: true
+  # vegafusion:
+  #    env:
+  #      UV_SYSTEM_PYTHON: true
 
-     strategy:
-       matrix:
-         python-version: ["3.11"]
-         os: [ubuntu-latest]
+  #    strategy:
+  #      matrix:
+  #        python-version: ["3.11"]
+  #        os: [ubuntu-latest]
 
-     runs-on: ${{ matrix.os }}
-     steps:
-        - uses: actions/checkout@v4
-        - uses: actions/setup-python@v5
-          with:
-            python-version: ${{ matrix.python-version }}
-        - name: Install uv
-          uses: astral-sh/setup-uv@v3
-          with:
-            enable-cache: "true"
-            cache-suffix: ${{ matrix.python-version }}
-            cache-dependency-glob: "**requirements*.txt"
-        - name: clone-vegafusion
-          run: |
-              git clone --single-branch -b v2 https://github.com/vega/vegafusion.git
-              cd vegafusion
-              git log
-        - name: Cache rust dependencies
-          uses: Swatinem/rust-cache@v2
-          with:
-            workspaces: vegafusion
-        - name: Build wheels
-          uses: PyO3/maturin-action@v1
-          with:
-            command: build
-            manylinux: 2014
-            rust-toolchain: stable
-            args: --release -m vegafusion/vegafusion-python/Cargo.toml --features=protobuf-src --strip
-        - name: Install wheels
-          working-directory: vegafusion/target/wheels/
-          run: |
-            ls -la
-            python -m pip install vegafusion-*manylinux*.whl
+  #    runs-on: ${{ matrix.os }}
+  #    steps:
+  #       - uses: actions/checkout@v4
+  #       - uses: actions/setup-python@v5
+  #         with:
+  #           python-version: ${{ matrix.python-version }}
+  #       - name: Install uv
+  #         uses: astral-sh/setup-uv@v3
+  #         with:
+  #           enable-cache: "true"
+  #           cache-suffix: ${{ matrix.python-version }}
+  #           cache-dependency-glob: "**requirements*.txt"
+  #       - name: clone-vegafusion
+  #         run: |
+  #             git clone --single-branch -b v2 https://github.com/vega/vegafusion.git
+  #             cd vegafusion
+  #             git log
+  #       - name: Cache rust dependencies
+  #         uses: Swatinem/rust-cache@v2
+  #         with:
+  #           workspaces: vegafusion
+  #       - name: Build wheels
+  #         uses: PyO3/maturin-action@v1
+  #         with:
+  #           command: build
+  #           manylinux: 2014
+  #           rust-toolchain: stable
+  #           args: --release -m vegafusion/vegafusion-python/Cargo.toml --features=protobuf-src --strip
+  #       - name: Install wheels
+  #         working-directory: vegafusion/target/wheels/
+  #         run: |
+  #           ls -la
+  #           python -m pip install vegafusion-*manylinux*.whl
 
-            # Optional dependencies
-            python -m pip install pyarrow pandas polars-lts-cpu "duckdb>=1.0" "vl-convert-python>=1.0.1rc1" scikit-image "pandas>=2.2" jupytext voila anywidget ipywidgets chromedriver-binary-auto
+  #           # Optional dependencies
+  #           python -m pip install pyarrow pandas polars-lts-cpu "duckdb>=1.0" "vl-convert-python>=1.0.1rc1" scikit-image "pandas>=2.2" jupytext voila anywidget ipywidgets chromedriver-binary-auto
 
-            # Test dependencies
-            python -m pip install pytest altair vega-datasets scikit-image jupytext voila ipykernel anywidget ipywidgets selenium flaky tenacity chromedriver-binary-auto 
-        - name: Test lazy imports
-          working-directory: vegafusion/vegafusion-python/
-          run: python checks/check_lazy_imports.py
-        - name: Test vegafusion
-          working-directory: vegafusion/vegafusion-python/
-          env:
-            VEGAFUSION_TEST_HEADLESS: 1
-          run: pytest
+  #           # Test dependencies
+  #           python -m pip install pytest altair vega-datasets scikit-image jupytext voila ipykernel anywidget ipywidgets selenium flaky tenacity chromedriver-binary-auto 
+  #       - name: Test lazy imports
+  #         working-directory: vegafusion/vegafusion-python/
+  #         run: python checks/check_lazy_imports.py
+  #       - name: Test vegafusion
+  #         working-directory: vegafusion/vegafusion-python/
+  #         env:
+  #           VEGAFUSION_TEST_HEADLESS: 1
+  #         run: pytest
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)