Skip to content

Add FromIterator for Py<PyList>#5822

Open
bschoenmaeckers wants to merge 1 commit intoPyO3:mainfrom
bschoenmaeckers:list-from-iter
Open

Add FromIterator for Py<PyList>#5822
bschoenmaeckers wants to merge 1 commit intoPyO3:mainfrom
bschoenmaeckers:list-from-iter

Conversation

@bschoenmaeckers
Copy link
Member

closes #4782

@bschoenmaeckers bschoenmaeckers force-pushed the list-from-iter branch 4 times, most recently from 8663422 to 7160959 Compare February 17, 2026 12:47
{
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
// TODO(bschoenmaeckers): Merge this impl with `PyList::new` when `try_as_dyn` is stable.
Python::attach(|py| {
Copy link
Member

Choose a reason for hiding this comment

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

I generally think we should avoid impls that internally need to call Python::attach. It's surprising and not really visible to the user and thus can easily lead to deadlocks. For the same reason we also removed the Clone impl of Py.

Adding this to Bound<PyList> could be an option however.

Copy link
Member

Choose a reason for hiding this comment

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

Another option is to require the item type to be Bound<'py, PyAny> - that way the items guarantee the thread is attached.

That requires callers to do the work with into_pyobject(), but that's possibly a good thing anyway because it would avoid the need for the Infallible constraint.

Copy link
Member

Choose a reason for hiding this comment

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

Great idea, that also works 👍

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 was also thinking about this. But I was concerned it might let users sneak out a python token using a empty iterator.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, great catch. The following indeed segfaults:

impl<'py> FromIterator<Bound<'py, PyAny>> for Bound<'py, PyList> {
    fn from_iter<T: IntoIterator<Item = Bound<'py, PyAny>>>(_iter: T) -> Self {
        PyList::empty(unsafe { Python::assume_attached() })
    }
}

#[test]
fn test_fabricate_list() {
    std::iter::empty().collect::<Bound<'_, PyList>>();
}

It's interesting to conclude that Python::assume_attached() is unsound in methods that take optional bound values - I think I had not previously realised this, and assumed the presence of the 'py lifetime in Option<Bound<'py, PyAny>> was enough to create soundness.

... searching the codebase briefly, I didn't spot any places where this is currently an issue.

Copy link
Member Author

@bschoenmaeckers bschoenmaeckers Feb 18, 2026

Choose a reason for hiding this comment

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

I could panic (maybe only in debug?) when a user tries to call this without a attached thread. And by only implementing on Py<PyList> we prevent leaking an unbounded python token. Would this be sufficient?

impl<'py> FromIterator<Bound<'py, PyAny>> for Py<PyList>
{
    fn from_iter<T: IntoIterator<Item = Bound<'py, PyAny>>>(iter: T) -> Self {
        let mut elements = iter.into_iter().peekable();
        let py = elements.peek().map_or_else(|| {
            assert!(thread_is_attached());
            unsafe { Python::assume_attached() }
        }, |item| item.py());

        PyList::new(py, elements)
            .expect("Could not allocate enough memory to add elements to the list")
            .unbind()
    }
}

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 think that adding the Infallible constraint would make this more flexible and makes it more obvious that this function does not unwrap conversion failures. What do you think?

impl<'py, A> FromIterator<Bound<'py, A>> for Py<PyList>
where
    Bound<'py, A>: IntoPyObject<'py, Error = Infallible>,

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'm also fine with the following:

impl<'py, A> FromIterator<Bound<'py, A>> for Py<PyList>
where
    Bound<'py, A>: IntoPyObject<'py>,

Copy link
Member

Choose a reason for hiding this comment

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

I have to say that personally I'm not convinced that we should add this under these circumstances. Just using PyList::new directly seems like the cleaner solution to me.

Copy link
Member

Choose a reason for hiding this comment

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

I think I'm coming to the same conclusion, especially after #4782 (comment) which we could document as an option for fallible iterators

Comment on lines 478 to 479
/// This may be slower than using [`PyList::new`], because it cannot reserve enough space ahead of
/// time when the iterator is not [`ExactSizeIterator`]
Copy link
Member

Choose a reason for hiding this comment

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

We should consider is relaxing the need for PyList::new to receive ExactSizeIterator, otherwise this implementation provides a (possibly unexpected) way to get around that.

Not sure of the pros/cons to doing that.

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 would love to relax the bound on PyList::new but this will make it impossible to implement the upper bound check that is currently in place. try_as_dyn would solve this in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we just drop this check and do the same thing as your FromIterator implementation by just calling append on the extra elements? This way we provide a more usable API and avoid the try_as_dyn requirement

Copy link
Member Author

Choose a reason for hiding this comment

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

We could drop the check, but let's see if other maintainers think that is desirable.

Copy link
Member

Choose a reason for hiding this comment

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

I think calling append on the extra elements is relatively fine (it's what Vec would do, after all), however I think the bigger downside is when there's fewer elements in the iterator than expected.

Because we have to set the list to the expected size to the list up-front, we'd need to "delete" the invalid entries off the end of the list, possibly first pre-filling with a sentinel value such as None.

I'm open to seeing how that looks, however that's probably fiddly enough it's worth doing in its own PR separate to this one.

I also am not sure if we can do similar for PyTuple (if we even should).

Copy link
Member Author

Choose a reason for hiding this comment

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

Can't we just trust the lower bound on size_hint and panic if the iterator produces less than that?

Copy link
Member

Choose a reason for hiding this comment

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

I guess that can work 👍

{
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
// TODO(bschoenmaeckers): Merge this impl with `PyList::new` when `try_as_dyn` is stable.
Python::attach(|py| {
Copy link
Member

Choose a reason for hiding this comment

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

Another option is to require the item type to be Bound<'py, PyAny> - that way the items guarantee the thread is attached.

That requires callers to do the work with into_pyobject(), but that's possibly a good thing anyway because it would avoid the need for the Infallible constraint.

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.

Implement IntoPyObject for Result<T, E> where E: Into<PyErr>

4 participants

Comments