Skip to content

Fix/from native update version#3515

Open
camriddell wants to merge 6 commits intonarwhals-dev:mainfrom
camriddell:fix/from_native-update-version
Open

Fix/from native update version#3515
camriddell wants to merge 6 commits intonarwhals-dev:mainfrom
camriddell:fix/from_native-update-version

Conversation

@camriddell
Copy link
Member

Description

Calling from_native(nw.DataFrame(…), version=…) would always return the passed DataFrame/Series object.
This ignores the version that is specified in the version argument.

Instead, for a given narwhals DataFrame/Series from_native now returns the same object if the versions match and creates a new object if the versions do not match.

Note, 2 clean up items are also added in this PR.

Before:

import narwhals as nw, narwhals.stable.v1 as nw_v1
import polars as pl

native = pl.DataFrame({"a": [1, 2, 3]})

unstable = nw.from_native(native)
stablified = nw_v1.from_native(unstable)

print(
    isinstance(stablified, nw_v1.DataFrame), # False
    isinstance(stablified, nw.DataFrame),    # True
    sep='\n'
)

stable = nw_v1.from_native(native)
unstablified = nw.from_native(unstable)

print(
    isinstance(unstablified, nw_v1.DataFrame), # False
    isinstance(unstablified, nw.DataFrame),    # True
    sep='\n'
)

After:

import narwhals as nw, narwhals.stable.v1 as nw_v1
import polars as pl

native = pl.DataFrame({"a": [1, 2, 3]})

unstable = nw.from_native(native)
stablified = nw_v1.from_native(unstable)

print(
    isinstance(stablified, nw_v1.DataFrame), # True
    isinstance(stablified, nw.DataFrame),    # True (since nw.v1.DataFrame is subclass of nw.DataFrame)
    sep='\n'
)

stable = nw_v1.from_native(native)
unstablified = nw.from_native(unstable)

print(
    isinstance(unstablified, nw_v1.DataFrame), # False
    isinstance(unstablified, nw.DataFrame),    # True
    sep='\n'
)

reported by hoxbro (Holoviews) on Discord #help channel

What type of PR is this? (check all applicable)

  • 💾 Refactor
  • ✨ Feature
  • 🐛 Bug Fix
  • 🔧 Optimization
  • 📝 Documentation
  • ✅ Test
  • 🐳 Other

Related issues

  • Related issue #<issue number>
  • Closes #<issue number>

Checklist

  • Code follows style guide (ruff)
  • Tests added
  • Documented the changes

in tests/translate/from_native_test.py
test_init_already_narwhals and test_init_already_narwhals_unstable
have the exact same source code, when the former should verify behavior
in a stable version.

This function used to test this, but seemed to have been mistakenly
edited.
from_native on a narwhals.{DataFrame,Series} would always return
the same object even if the DataFrame._version and the version
passed into from_native disagreed.

Now, if the versions mismatch we create a new narwhals object
with the appropriate version. If the versions match, we short cut
and return the same object.
@camriddell camriddell force-pushed the fix/from_native-update-version branch from 761f14c to f1cf356 Compare March 18, 2026 19:01
```
"""

_version: ClassVar[Version] = Version.MAIN
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hoping this has been an oversight and wasn't excluded for a strong reason. This change makes the DataFrame/LazyFrame api a bit more consistent internally.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah nice one, thanks @camriddell!

If you dig through the blame (I think) I added these on Series & DataFrame at the same time as the from_* constructors.

LazyFrame didn't get any new methods, so it slipped through 😅

@camriddell
Copy link
Member Author

@MarcoGorelli I believe the nightly failure isn't due to this change but perhaps something that needs to be accounted for in pandas. Shall I open up an Issue with this? Or do you believe it is relevant to this PR?

pl_frame = pl.DataFrame({"a": [1, 2, 3]}).lazy()
nw_frame = nw_v1.from_native(pl_frame)
with pytest.raises(AttributeError):
with pytest.raises(AssertionError):
Copy link
Member

@dangotbanned dangotbanned Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, there may be a comment somewhere mentioning the error?

(Ignore me if this is too vague 😂)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this comment in the comparable tests for DataFrames

    # NOTE: (#2629) combined with passing in `nw_v1.DataFrame` (w/ a `_version`) into itself changes the error
    with pytest.raises(AssertionError):
        nw_v1.DataFrame(nw_frame, level="full")

The LazyFrame had an AttributeError because the LazyFrame.__init__ would try to check the ._version attribute which didn't exist. This change more closely aligns this LazyFrame with its DataFrame and Series counterparts since they all successfully fail with the same error now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

successfully fail

😂

@dangotbanned
Copy link
Member

@camriddell I should probably check what was mentioned on discord - but from reading the PR I'm a bit confused on the problem that's being solved.

(#3515 (comment)) is all good - but something seems strange to me that calling from_native changes the version 🤔

FWIW, I don't think from_native should accept narwhals-level objects, but we are where we are 😅

@MarcoGorelli
Copy link
Member

MarcoGorelli commented Mar 21, 2026

thanks @camriddell !

on board with the suggestions, i think this just needs addressing

tests/translate/from_native_test.py:242: error: Need type annotation for
"stablified_s"  [var-annotated]
        stablified_s = nw_v1.from_native(unstable_s, allow_series=True)
                       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tests/translate/from_native_test.py:257: error: Need type annotation for
"stable_s"  [var-annotated]
        stable_s = nw_v1.from_native(s, allow_series=True)
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
tests/translate/from_native_test.py:258: error: Need type annotation for
"unstablified_s"  [var-annotated]
        unstablified_s = nw.from_native(stable_s, allow_series=True)
                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Found 3 errors in 1 file (checked 469 source files)
make: *** [Makefile:26: typing] Error 1

the nightly failure is just due to pandas-dev/pandas#64749 and is unrelated to your pr

@camriddell
Copy link
Member Author

@camriddell I should probably check what was mentioned on discord - but from reading the PR I'm a bit confused on the problem that's being solved.
(#3515 (comment)) is all good - but something seems strange to me that calling from_native changes the version 🤔

The primary concern here is surprise that the v1.from_native can give you a non-v1 object back. The current fast path implementation means that if you call: v2.from_native(v1.DataFrame) you get back the same v1.DataFrame, which is odd because every other input type will give you back a v2.DataFrame. Meaning the signature of v2.from_native return type is "usually a v2 object, but sometimes whatever you passed into it".

FWIW, I don't think from_native should accept narwhals-level objects, but we are where we are 😅

I agree with this from a structurally-correct perspective. However, conveniences are nice for DX especially since our primary audience is dataframe-agnostic libraries. This code let's them treat any incoming frame the same way without having to manually check if it is already a narwhals DataFrame. Likely their end users won't be passing in narwhals DataFrames, but they could have multiple entry points may need to translate to a narwhals object and our "recursive" type support means they don't have to write explicit isinstance checks before every from_native call.

@camriddell camriddell force-pushed the fix/from_native-update-version branch from b596ed5 to 835c683 Compare March 23, 2026 01:53
@dangotbanned
Copy link
Member

(#3515 (comment))

Thanks for the context @camriddell!

I have a couple more questions, are you okay if we sit on this until later today?

@camriddell
Copy link
Member Author

(#3515 (comment))

Thanks for the context @camriddell!

I have a couple more questions, are you okay if we sit on this until later today?

Absolutely! I'm not in a rush to merge.

@dangotbanned
Copy link
Member

dangotbanned commented Mar 23, 2026

@camriddell thanks for your patience 😄

1. Similar issue

Did you know about this one?

2. What's the worst that could happen?

I'm curious if we can reveal cases downstream that are (unknowingly?) relying on this behavior.

As an example, if I change tests/v1_test.py::test_with_row_index like this on main - it passes through no problemo.
But wouldn't that be a v1 break following this PR?

Show diff

diff --git a/tests/v1_test.py b/tests/v1_test.py
index 330211162..59ccdae61 100644
--- a/tests/v1_test.py
+++ b/tests/v1_test.py
@@ -447,7 +447,7 @@ def test_with_row_index(constructor: Constructor) -> None:
         pytest.skip()
     data = {"abc": ["foo", "bars"], "xyz": [100, 200], "const": [42, 42]}
 
-    frame = nw_v1.from_native(constructor(data))
+    frame = nw.from_native(nw_v1.from_native(constructor(data)))
 

What happens if you try running the CI but change the let's see who this really is to raising an exception?

3. Is this discoverable?

Part of what I was getting at in (#3515 (comment)) is that this looks like 2 user errors to me:

# (1) Mixed versions
# (2) Asking for something you already have
nw.from_native(nw_v1.DataFrame(...))

If you think the feature to switch versions is something we need, wouldn't this be a better fit?:

nw_v1.DataFrame(...).to_version("main")
nw_v1.DataFrame(...).to_version("v1")
nw_v1.DataFrame(...).to_version("v2")

There's less ambiguity with something along those lines IMO - and it avoids defining new behavior for from_native

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants