Repository navigation
Interned strings are immortal, despite what the documentation says #113993
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Jan 12, 2024 I would suggest that using "immortal" here could be confusing. The flagged checked by
_Py_IsImmortal()is low-level implementation detail and should not be something that the library documentation discusses. Strings that you callintern()on may or may not have this flag set. Different Python runtimes might implementintern()some other way. The behavior that matters is that the string gets stored in an interned strings table and that future calls tointern()with the same string content will return an object with the sameid(), even if the user program doesn't keep a reference to the string.Reacted by EloffThe behavior that matters is that the string gets stored in an interned strings table and that future calls to intern() with the same string content will return an object with the same id(), even if the user program doesn't keep a reference to the string.
No. That is not the behaviour on Python 3.11.
I get:$ python3.11 Python 3.11.7 (main, Dec 18 2023, 00:00:00) [GCC 13.2.1 20231205 (Red Hat 13.2.1-6)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> id1 = id(sys.intern('some string')) >>> other = sys.intern('other string') >>> id2 = id(sys.intern('some string')) >>> id1 == id2 FalseIMO, the guarantee is that if two strings are interned, they can be compared by identity.
CPython uses this for a fast path in string comparison; so some users callinternon strings that are likely to be compared to one another -- even ones that srent expected to live until interpreter shutdown. We can say that's optmizing for a specific implementation, but it's common -- evenpathliband the compiler (e.g.eval) do it.The new behaviour can leak strings that are not likely to be used again.
$ cat test.py import sys id1 = id(sys.intern('some string')) other = sys.intern('other string') id2 = id(sys.intern('some string')) assert id1 == id2 $ python3.8 ./test.py $ python3.9 ./test.py $ python3.10 ./test.py $ python3.11 ./test.py $ python3.12 ./test.py $ python3.13 ./test.py
No assertions triggered.
Reacted by EloffThe the new string is allowed to reuse the old one's memory; I guess this is build-specific. What platform are you on?
Instead ofother = sys.intern('other string'), try putting ingc.collectand something more intensive, likeimport asyncio?I'm on macOS using the official build. Strange thing: I can reproduce the behaviour you're seeing in the REPL.
Instead of
other = sys.intern('other string'), try putting ingc.collectand something more intensive, likeimport asyncio?Nope, cannot reproduce using
$ python3.11 ./test.pyas above.Ah! Right, literal constants are part the code object, which does normally stay around. You'll need something that's not folded to a constant, like:
$ cat test.py import sys def intern_and_report(string): string = sys.intern(string) the_id = id(string) print(f'{string!r} at {id(string)}: refcount {sys.getrefcount(string):x}') return the_id id1 = intern_and_report('a' + __name__) str_b = sys.intern('b' + __name__) id2 = intern_and_report('a' + __name__) assert id1 == id2
$ python3.10 ./test.py 'a__main__' at 139775546163056: refcount 3 'a__main__' at 139775546294640: refcount 3 Traceback (most recent call last): File "/tmp/rttair/./test.py", line 11, in <module> assert id1 == id2 AssertionError $ python3.12 ./test.py 'a__main__' at 139964910181616: refcount ffffffff 'a__main__' at 139964910181616: refcount ffffffffReacted by Erlend E. AaslandYes; thanks!
See also #113601
@encukou just getting to this! You're right, as of now, all interned strings should be immortal, so the
sys.interndocs should probably be updated to update that they are indeed immortal and the fact that we don't need to keep a reference to benefit from it anymore.As for the leaks, as @erlend-aasland already called out, we have a PR to correctly clean them up in #113601!
Reacted by Erlend E. AaslandThe behavior that matters is that the string gets stored in an interned strings table and that future calls to intern() with the same string content will return an object with the same id(), even if the user program doesn't keep a reference to the string.
No. That is not the behaviour on Python 3.11.
Sorry, I was not very clear. I was describing the behaviour we have with Python 3.12 and we had with older versions of Python (version 2.2 and before, it seems). It looks like this commit changed it: 45ec02a.
Since 3.12 changed this, definitely the documentation should be updated. Further, I think we should have had a discussion and explicitly decide if this is desirable behaviour, since it reverts the effect of 45ec02a. Maybe there was one and I missed it? Perhaps free-threading doesn't require that every interned string become immortal but only some of them (e.g. symbols used by code objects etc). That might preserve free-threaded performance (e.g. interned strings shared between threads) while avoid the "leak" like encountered by pathlib (e.g. interned strings only used for a short time and then discarded).
Reacted by Petr Viktorin and Inada Naokias of now, all interned strings should be immortal
To be clear, I think this is bad. Some interned strings are temporary, and they now leak. They should be cleaned up sooner that at interpreter shutdown.
Reacted by Mark Shannon, Inada Naoki, Jeffrey Bosboom and Patrik Veselý35 remaining items
- added a commit that references this issue
on Jul 17, 2024 The 3.12 backport is at #123065.
@Yhg1s, re:
Yes, I agree this should be reverted in 3.12, and conditional on free threading in 3.13. That's my opinion as RM, but considering the discussions the DC had about the immortal objects PEP, I'm sure they all agree.
Unfortunately it's far from a straightforward revert.
Do you still want this in 3.12? Should I ask the SC?
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Bug report
Bug description:
The
sys.interndocumentation explicitly says:However, they were made immortal in #19474 (Implement Immortal Objects -- implementation of PEP 683), without a documentation update. The 3.12 What's New also doesn't mention the change. [edit: it's now clear that the PR author (@eduardo-elizondo) intended it but the reviewer (@markshannon) did not]
PEP-683 itself only mentions the change as a possible optimization.
Meanwhile,
pathlibinterns every path segment it processes, presumably depending on the documented behaviour. In CPython's test suite, that causes names of temporary directories to leak (stealthily, since interned strings are excluded from the total refcount).[edit: Worse,
type_setattro,PyObject_SetAttrorPyDict_SetItemStringimmortalize keys, so strings used for key/attribute names this way are not reclaimed until interpreter shutdown.]On the other hand, free-threading (PEP-703)
seems to rely[edit: relies] on these being immortal.What's the correct behaviour? [edit: specifically, in 3.12 and non-free-threading 3.13, should this change be reverted or documented?]
@eduardo-elizondo @ericsnowcurrently
Linked PRs
Related fixes: