Skip to content

Defining iterator in a separate class no longer works in 3.13 #128161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
antonio-rojas opened this issue Dec 21, 2024 · 20 comments
Open

Defining iterator in a separate class no longer works in 3.13 #128161

antonio-rojas opened this issue Dec 21, 2024 · 20 comments
Labels
3.13 bugs and security fixes 3.14 bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@antonio-rojas
Copy link

antonio-rojas commented Dec 21, 2024

Bug report

Bug description:

Defining an interator for a class in a separate class no longer works properly in 3.13. With the following test_iter.py:

class list2(list):
    def __iter__(self):
        return list2iterator(self)

class list2iterator:
    def __init__(self, X):
        self._X = X
        self._pointer = -1

    def __next__(self):
        self._pointer += 1
        if self._pointer == len(self._X):
            self._pointer = -1
            raise StopIteration
        return self._X[self._pointer]

With Python 3.13.1 one gets:

>>> from test_iter import list2
>>> X=list2([1,2,3])
>>> [x for x in X]
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    [x for x in X]
                ^
TypeError: 'list2iterator' object is not iterable

With Python 3.12.7 it works:

>>> from test_iter import list2
>>> X=list2([1,2,3])
>>> [x for x in X]
[1, 2, 3]

Bisected to bcc7227e

CPython versions tested on:

3.12, 3.13

Operating systems tested on:

Linux

Linked PRs

@antonio-rojas antonio-rojas added the type-bug An unexpected behavior, bug, or error label Dec 21, 2024
@Eclips4
Copy link
Member

Eclips4 commented Dec 21, 2024

@ronaldoussoren
Copy link
Contributor

https://docs.python.org/3/library/stdtypes.html#iterator-types says that iterators must also implement __iter__ (returning self). That's a long standing requirement on iterators, and not one introduced in 3.13.

@antonio-rojas
Copy link
Author

Alright, thanks

@picnixz picnixz closed this as not planned Won't fix, can't repro, duplicate, stale Dec 21, 2024
@Eclips4
Copy link
Member

Eclips4 commented Dec 22, 2024

@ronaldoussoren You're right, and this code is definitely doing something weird, but it works in 3.12:
example.py:

class list2(list):
    def __iter__(self):
        return list2iterator(self)

class list2iterator:
    def __init__(self, X):
        self._X = X
        self._pointer = -1

    def __next__(self):
        self._pointer += 1
        if self._pointer == len(self._X):
            self._pointer = -1
            raise StopIteration
        return self._X[self._pointer]


target = list2([1, 2, 3])
for i in target:
    print(i)


[print(i) for i in target]

3.12.8:

eclips4@suffering ~/tmp> python3.12 example.py
1
2
3
1
2
3

3.13.1:

1                                                                                  
2
3
Traceback (most recent call last):
  File "/Users/eclips4/tmp/example.py", line 23, in <module>
    [print(i) for i in target]
                       ^^^^^^
TypeError: 'list2iterator' object is not iterable

Though it probably shouldn't work, it is definitely a regression.

Bytecode for [x for x in y] 3.12.8:

  0           0 RESUME                   0

  1           2 LOAD_NAME                0 (y)
              4 GET_ITER
              6 LOAD_FAST_AND_CLEAR      0 (x)
              8 SWAP                     2
             10 BUILD_LIST               0
             12 SWAP                     2
        >>   14 FOR_ITER                 4 (to 26)
             18 STORE_FAST               0 (x)
             20 LOAD_FAST                0 (x)
             22 LIST_APPEND              2
             24 JUMP_BACKWARD            6 (to 14)
        >>   26 END_FOR
             28 SWAP                     2
             30 STORE_FAST               0 (x)
             32 RETURN_VALUE
        >>   34 SWAP                     2
             36 POP_TOP
             38 SWAP                     2
             40 STORE_FAST               0 (x)
             42 RERAISE                  0
ExceptionTable:
  10 to 26 -> 34 [2]

3.13.1:

   0           RESUME                   0

   1           LOAD_NAME                0 (y)
               GET_ITER
               LOAD_FAST_AND_CLEAR      0 (x)
               SWAP                     2
       L1:     BUILD_LIST               0
               SWAP                     2
               GET_ITER  - problematic instruction
       L2:     FOR_ITER                 4 (to L3)
               STORE_FAST_LOAD_FAST     0 (x, x)
               LIST_APPEND              2
               JUMP_BACKWARD            6 (to L2)
       L3:     END_FOR
               POP_TOP
       L4:     SWAP                     2
               STORE_FAST               0 (x)
               RETURN_VALUE

  --   L5:     SWAP                     2
               POP_TOP

   1           SWAP                     2
               STORE_FAST               0 (x)
               RERAISE                  0
ExceptionTable:
  L1 to L4 -> L5 [2]

@Eclips4 Eclips4 reopened this Dec 22, 2024
@Eclips4 Eclips4 added 3.13 bugs and security fixes 3.14 bugs and security fixes labels Dec 22, 2024
@ronaldoussoren
Copy link
Contributor

@ronaldoussoren You're right, and this code is definitely doing something weird, but it works in 3.12: example.py:

Right, the code from the OP is broken according to the data model, but we still did a change that's not backward compatible. I'm not deep enough into this to fully understand the ramifications.

@picnixz picnixz added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Dec 22, 2024
@efimov-mikhail
Copy link
Contributor

This behavior change is a direct consequence of change in bytecode generation in my PR. I don't think this is a bug.

@terryjreedy
Copy link
Member

The exact example worked in the CPython implementation of 3.12, but that may be an accident, as it should not have by the language definition. It might have already failed in 3.12 in other implementations. In any case, adding the following exposes the defect in list2iterable even in 3.12.

target2 = iter(target)  # A list2iterator object
target3 = iter(target2)  # Fails in 3.12.8 
# TypeError: 'list2iterator' object is not iterable

If there is still a question about changing anything, Guido might remember the original design discussions.

@pochmann3
Copy link
Contributor

Relevant bit from glossary: iterator:

CPython implementation detail: CPython does not consistently apply the requirement that an iterator define __iter__().

@terryjreedy
Copy link
Member

Right. A defective iterator may or may not work, so skip the requirement at your own risk. Int caching is another implementation detail. Implementation details are 1. subject to change and 2. are sometimes noted to discourage the filing of issues. I think the iterator note followed a discussion and decision that the inconsistency should not be treated as a bug. But the note also reaffirms the requirement. Removing it, at least for for-loops, would have required other implementations and future CPythons to allow the omission. All that is needed for list2iterator is def __iter__(self): return self. This is not a great burden. Its presence would make list2iterator also work for iter(list2iterator(somelist)).

@rhettinger
Copy link
Contributor

FWIW, the OP's code also works in PyPy3, so this is more than a C implementation detail.

@serhiy-storchaka
Copy link
Member

I think that the original change was not needed snd should be reverted. It just slows dowsn everyone's code.

HinTak added a commit to HinTak/skia-python that referenced this issue Dec 29, 2024
See
python/cpython#128161
"Defining iterator in a separate class no longer works in 3.13"

We have iterator for SkTextBlob defined by SkTextBlob::Iter(textblob),
which is the c++/pybind11 equivalent of the same situation.
Following the suggestion:
python/cpython#128161 (comment)

Also see actions/runner-images#11241

Fixes kyamagu#295
@HinTak
Copy link

HinTak commented Dec 29, 2024

Well, I am somewhat unhappy about this as we have 4-year old code that works on all python versions from 3.8 to 3.13 until last month; then it broke with 3.13.1; I thought mistakenly that github did something stupid in their CI images between 3.13 and 3.13.1. Never thought that python would break existing code within a minor version upgrade...

@picnixz
Copy link
Member

picnixz commented Dec 29, 2024

Considering we broke something in a minor release, we should probably consider reverting the change. In addition, we ourself say that we don't consistently apply the requirement on iterators.

So even if it's a data model requirement... I think it would be nicer to users. Considering the bytecode generation stuff was for a probably rarer usecase, I would prefer the above code to work instead.

HinTak added a commit to HinTak/skia-python that referenced this issue Jan 13, 2025
See
python/cpython#128161
"Defining iterator in a separate class no longer works in 3.13"

We have iterator for SkTextBlob defined by SkTextBlob::Iter(textblob),
which is the c++/pybind11 equivalent of the same situation.
Following the suggestion:
python/cpython#128161 (comment)

Also see actions/runner-images#11241

Fixes kyamagu#295
HinTak added a commit to HinTak/skia-m1xx-python that referenced this issue Mar 13, 2025
See
python/cpython#128161
"Defining iterator in a separate class no longer works in 3.13"

We have iterator for SkTextBlob defined by SkTextBlob::Iter(textblob),
which is the c++/pybind11 equivalent of the same situation.
Following the suggestion:
python/cpython#128161 (comment)

Also see actions/runner-images#11241

Fixes #295
@efimov-mikhail
Copy link
Contributor

I think we should decide what to do with this issue.
FYI, on current CPython 3.12 branch this code snippet works, but on 3.13, 3.14 and main branches it doesn't.

@picnixz @Eclips4 @markshannon

Basically, there are two ways which I can see as reliable:

  1. Just close the issue w/o any changes in codebase.
  2. Provide changes to 3.13 branch only just to fix some "very big change for minor release".

And some links on related issues and PRs:
#125038
#125178
#132351

@efimov-mikhail
Copy link
Contributor

efimov-mikhail commented May 26, 2025

And maybe it's reasonable to do some changes in docs?
Some clear mention about generator expressions like (x for x in None), for example.
They aren't correct in Python 3.13.3 and before, an exception will be raised on this expression in code.
But on current 3.13 branch this code is correct.

Maybe it's better to remove all related changes in 3.13 branch?
And have exactly the same behavior as on 3.12?

@hugovk @terryjreedy @JelleZijlstra

@donhatch
Copy link

donhatch commented May 27, 2025

The issue title could perhaps be improved.

Current title: "Defining iterator in a separate class no longer works in 3.13"
Suggested new title: "Iterator without __iter__ no longer works in 3.13.1"

Details:

  • If I understand correctly, the iterator being defined in a separate class isn't really relevant; what's very relevant is that it doesn't have an __iter__ member function
  • it's significant that the version in which this broke is minor release 3.13.1, not 3.13 (I observe the example code succeeds in 3.13.0)

@donhatch
Copy link

The title and text of committed #132351 seems to imply that maybe it fixes this, i.e. reverts the behavior to pre-3.13.1 in which the example code works (because the generated bytcode has no redundant GET_ITER on something already known to be an iterator).

Is that correct?

That would make me very happy, since I would very much like to go back to happily ignoring the IMO ill-advised "requirement" that iterators be iterable (in fact it is my hope that some day that requirement might be officially removed, for a brighter less-confusing future in which Iterator and Iterable are two clearly distinct non-overlapping concepts).

@donhatch
Copy link

@efimov-mikhail wrote:

And maybe it's reasonable to do some changes in docs?
Some clear mention about generator expressions like (x for x in None), for example.
They aren't correct in Python 3.13.3 and before, an exception will be raised on this expression in code.
But on current 3.13 branch this code is correct.

(Unfortunately I don't currently have anything higher than 3.13.0 to try this out on...)

In 3.13.0 I see that (x for x in None) throws TypeError: 'NoneType' object is not iterable which seems reasonable to me.
Are you saying that, in later versions, it returns something instead of throwing, and that it's correct? Why?

@efimov-mikhail
Copy link
Contributor

efimov-mikhail commented May 27, 2025

The title and text of committed #132351 seems to imply that maybe it fixes this, i.e. reverts the behavior to pre-3.13.1 in which the example code works (because the generated bytcode has no redundant GET_ITER on something already known to be an iterator).

Is that correct?

No, this is not correct.
On current main branch

-> % cat test_list2.py
class list2(list):
    def __iter__(self):
        return list2iterator(self)

class list2iterator:
    def __init__(self, X):
        self._X = X
        self._pointer = -1

    def __next__(self):
        self._pointer += 1
        if self._pointer == len(self._X):
            self._pointer = -1
            raise StopIteration
        return self._X[self._pointer]

[print(i) for i in target]

-> % ./python test_list2.py
Traceback (most recent call last):
  File "/home/sikko/projects/cpython/test_iter.py", line 21, in <module>
    [print(i) for i in target]
                       ^^^^^^
TypeError: 'list2iterator' object is not iterable

>>> dis.dis('[print(i) for i in target]')
   0           RESUME                   0

   1           LOAD_NAME                0 (target)
               GET_ITER
               LOAD_FAST_AND_CLEAR      0 (i)
               SWAP                     2
       L1:     BUILD_LIST               0
               SWAP                     2
               GET_ITER
       L2:     FOR_ITER                11 (to L3)
               STORE_FAST               0 (i)
               LOAD_NAME                1 (print)
               PUSH_NULL
               LOAD_FAST_BORROW         0 (i)
               CALL                     1
               LIST_APPEND              2
               JUMP_BACKWARD           13 (to L2)
       L3:     END_FOR
               POP_ITER
       L4:     SWAP                     2
               STORE_FAST               0 (i)
               RETURN_VALUE

  --   L5:     SWAP                     2
               POP_TOP

   1           SWAP                     2
               STORE_FAST               0 (i)
               RERAISE                  0
ExceptionTable:
  L1 to L4 -> L5 [2]

This is because of changes in bytecode.
You can see two GET_ITER bytecodes for list comprehension here.

Suggested new title: "Iterator without __iter__ no longer works in 3.13.1"

It sounds reasonable for me. Although, this is not the only change.

(Unfortunately I don't currently have anything higher than 3.13.0 to try this out on...)

In 3.13.0 I see that (x for x in None) throws TypeError: 'NoneType' object is not iterable which seems reasonable to me. Are you saying that, in later versions, it returns something instead of throwing, and that it's correct? Why?

Yes, on current main, 3.14 and 3.13 branches we can see the following result:

>>> g = (x for x in None)
>>> g
<generator object <genexpr> at 0x7f923bb48130>
>>> list(g)
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    list(g)
    ~~~~^^^
  File "<python-input-2>", line 1, in <genexpr>
    g = (x for x in None)
                    ^^^^
TypeError: 'NoneType' object is not iterable

You can see some motivation here:
#125178 (comment)
#132351 (comment)

cc @markshannon @sobolevn

@Roman513
Copy link

Roman513 commented Jun 8, 2025

This is fixed in 3.13.4:

Python 3.13.4 (main, Jun  4 2025, 17:14:13) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from test_iter import list2
>>> X=list2([1,2,3])
>>> [x for x in X]
[1, 2, 3]

But in 3.14.0b2 it sill does not work.

However it could be reintroduced again, related issue #135171 and fix #135225

If there's a decision how this should work, do we need some additional tests to make the behavior stable?
cc: @picnixz @markshannon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 bugs and security fixes 3.14 bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy