Presumably the attack surface of “create a dictionary with all-colliding integer keys” isn’t considered significant (in contrast with “create a dictionary with all-colliding string keys”, which can easily be accomplished in a number of ways). Good luck utilizing the attack surface of “create a dictionary with all-colliding None keys”.
Ah, I hadn’t seen them there. One of the problems with splitting discussions is that not every participant has read every single post in every shard of the discussion. In any case…
This last one IS a very good point, and does leave one wondering then whether security has ANY significance whatsoever to this discussion. If practical considerations can allow a large set of integers to have constant hashes, even though the same consideration could have been achieved in other ways (for instance, guaranteeing that integers are interned, and then still using random values associated with them), then I dispute that there is any meaning to Steven’s concerns that a constant hash for None would be a risk.
So ultimately what we have is that a constant hash for None is special-casing it. That is, as far as I can tell, the ONLY downside, with an additional (weak) secondary argument from the slippery-slope of “maybe we should special-case other constants”. The OP has shown exactly how little code it takes to do this. Are there any other real downsides?
This has (some, small) practical benefit, as has been demonstrated. What are the real problems here?
The practical benefit is just the consistent set iteration order, right? The problem with that is that it is not really achieved with this change, because set doesn’t guarantee that consistent hashes will lead to consistent iteration order, even though it is implemented that way right now. Since it is an implementation detail, it can change in the next 3.12 alpha, and the order might depend on something random instead of the hash, and then you would be back to having inconsistent iteration order.
The real fix for the set iteration problem would be to either make sets ordered like we did for dictionaries, or provide a separate OrderedSet class and tell people to use that when they want consistent ordering of iteration.
Yes, consistent iteration order and everything that goes along with that. Is it actually likely that the implementation detail will change, though? Bear in mind that “consistent” wouldn’t break if the iteration order changes between versions (as it would still be consistent on repeated runs on the same version). The only way it would break is if it’s deliberately randomized; in the highly unlikely event that this happens, it will break other things than None, and that will have to be considered when it happens.
Bluntly, that throwing thousands of words at people about a change that has already been rejected isn’t a good way of persuading anyone.
The original thread here took 11 days and 10 messages to become a PR. 3 days later, a core developer politely thanked the poster and rejected the PR with an explanation. That’s an extremely fast turnaround for a CPython change proposal. We’re now up to more than 50 follow-up messages that are little more than re-hashing[1] the arguments that were made for the original proposal and were rejected.
Is this change important enough to override a core developer’s decision? That’s the real question now. I don’t think it is[2].
And that’s what I said at the beginning of this iteration of the discussion: that a fundamental problem here is the weaponization of exhaustion. Discussions here are wielded as a means of tiring out anyone who has a proposal, thus ensuring that a huge number of potentially-excellent proposals never get beyond the phase of “let’s see if you can respond to the same arguments a hundred times without getting snippy, because if you get snippy we deem you to have lost the argument anyway”.
It’s unfortunate that there’s been so much misunderstanding in all these threads.
The point isn’t guaranteed set iteration order, and the problem isn’t solved by turning off address randomization.
The point is to increase the scope for reproducibility. If you run a test and the test fails, you’d like to run it again knowing things will happen exactly the same way. Even with address randomization turned off, there’s no guarantee that the binary will get loaded to the same point in memory, so Nones hash will change. This can change various things, like the iteration order of sets or other data structures.
Allowing this reproducibility for testing is a very common use case and good practice; it’s why test frameworks often let you feed in the seed used for RNG from the command line and why python already has an environmental variable that makes the current salts applied to strings reproducible.
None is a fundamental building block that is often a member of e.g. dataclasses that otherwise have value semantics and are very useful for hashing. Because None is a potential member of Optional, and such dataclasses often have Optional members. That’s why None is practically speaking very different from other objects that use the default address based hashing.
Yes, giving None a fixed hash or a hash derived from the salt env variable (can’t remember the name) doesn’t fully solve this problem because address based hashing can still be used. But a) it’s still an improvement, and b) at least that will restrict these issues to what users would typically consider identity based hashing, and not let it spread to what most folks thing of as structural hashing.
It’s been proven that tenacity in the face of opposition is crucial to a proposal’s success. Proven time and again with successful proposals, not just failed ones. Are you surprised, then, when one rejection doesn’t kill a proposal instantly?
That’s exactly my point. It’s a gauntlet, not a technical discussion.
I’m done now. Go ahead, mark the thread as closed, it’s another proposal killed.
But it’s been rejected for totally flawed reasoning? Iirc the GitHub close on the original PR said that there was no use case or something to that effect. Reproducibility for testing purposes was mentioned from the get go, and it’s a completely standard and recognized use case.
If it was rejected by saying that the use case just isn’t important enough, or that it doesn’t completely enough solve the use case, or that the costs were prohibitive , then even if I disagreed I wouldn’t bother commenting. But it’s a bit disturbing to see things shut down so quickly by folks who didn’t understand the issue (or at least, I saw no evidence the issue is understood based on the close comment).
Maybe there should be a function for putting an unordered collection into a reproducibly-ordered list. That’s what sorted used to do at least for standard built in types in Python 2:
>>> sorted([None, 1, 2])
[None, 1, 2]
That was changed so now in 3.x:
>>> sorted([None, 1, 2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'int' and 'NoneType'
There were good reasons for making that change but there are also situations where the old behaviour is exactly what is wanted. If you had an ordered function for sorting a collection into an arbitrary but reproducible ordering then you could use that to iterate over a set in situations where the order might matter.
The comment “There is nothing special about None in this regard” pretty clearly to me says this isn’t important enough to be a special case.
Would it help if I (as another core developer) said the use case isn’t important enough? I’ve already said that I don’t think the issue is important enough to override @rhettinger, but I can say it again. And we’ve already had another core dev say (in the original thread) “IMO it was a good suggestion to explore, but ultimately, looks like isn’t worth it. I’m sorry if it looks like we lead you down the wrong path”.
I don’t know what more people can say. The arguments that have been made have been read and listened to. And rejected. Sorry, but people aren’t compelled to agree with a proposal. And repeating the same reasoning won’t alter that.
I have personally read every comment on this thread. They haven’t convinced me. If they had, I would have debated the rejection on github. But I agree with the rejection. I have spent a lot of time on this proposal, so I object to seeing the discussion characterised as “it’s a bit disturbing to see things shut down so quickly by folks who didn’t understand the issue”.
To give an example of the sort of thing that’s already been said, the document for the proposal says
For some real world use cases, it is needed (or very helpful) to have the capability to replay previously executed Python code on an identical input and expect an identical outcome. The use cases I’ve seen for this are (i) debugging the behavior of a complex/deep compute-only program, in domains such as operations research and compilation (ii) getting reproducible behavior in research notebooks, that other people can run for themselves and are assured to get identical results
OK. That’s a statement I can think about. My response is as follows:
If it is needed that identical input gives identical output, then relying on behaviour which is not guaranteed by the language is a bug. Maybe a latent bug, but a bug nevertheless. Maybe a bug so small that you don’t care, but a bug nevertheless. So fix the bug, if you care. The fact that your code has a bug isn’t a reason to claim that Python needs to change.
So we’re left with the possibility that it’s helpful to be able to re-run the code and get the same output. Sure. But apparently it’s not helpful enough to write your code to be give reproducible output from Python as it currently works. We don’t know what the OP’s code is - it’s presumably a large, complex system (given that it had a weird, hard to reproduce bug). And in this one instance, debugging the underlying problem would have been easier if the hash of None had not changed between runs. But what about all the other weird, hard to reproduce bugs that plague large and complex systems? Anyone maintaining such a system must have had bugs that they couldn’t reliably reproduce many, many times. What makes this example special enough to justify a change to Python? Is it simply that it’s a “small” change, so the justification doesn’t have to be very strong?
So basically, the argument here is that the change is small, and it might help the occasional debugging exercise in the future that happens to be similar to the one the OP encountered. One the the OP fixed without this change, albeit with more pain than would have been needed with the change.
Everything else is a matter of judgement. Someone has to judge the ultimate cost of the change. Someone has to judge how often this is likely to help in future. Someone has to balance those things. Making those judgements is the core developers’ (or ultimately the SC’s) job. Presenting the facts to allow them to make that judgement as best as they can is the proposer’s job. The discussions here should be aimed at helping the proposer collect those facts.
I don’t think the OP did a bad job of collecting facts here. The PR and associated issue were well written (maybe the pitch was a bit wordy, but given the length of this post, I’d be a hypocrite to complain about that ). And unfortunately for the OP, the decision was “thank you, but no”.
But we’re now in a situation where people are trying to appeal that decision. Not by presenting extra facts that weren’t covered in the proposal, but by claiming that the decision was insufficiently well thought through, or by criticising people’s attempts to explain their views. None of that is helpful. Everyone involved is a volunteer, and criticising what they say as not being in good faith is not the sort of tone we should be aiming for.
This is not about making a proposal “run the gauntlet” before it can be considered. The proposal was presented, considered, and a decision given. This is now solely about people not wanting to accept the decision that was made. And not being able to move on at this point reflects badly on all of us.
I don’t think it reflects badly on us. I think the topic keeps going because people’s opinions on this matter depend on some pretty foundational beliefs on how systems are ought to behave. Part of the discussion is deeper than this particular change request on CPython. It also explains the agitation I and other people have had with “how can they not see that???”
there are strong convictions all around.
A large system like this doesn’t actually depend on its own determinism for its correctness like you say, so it is not a really a bug. At least, I’ve never seen such a thing, in decades of working on software by the way. If your program uses sets, it does not make assumptions about the possible order given that it accepts arbitrary data. Data changes and the order changes. The behavior is too complicated to be relied upon even if you tried to do it intentionally.
To me that is completely obvious, I still saw several people arguing as if they really believe this to be real world practice. I’ve never seen it. As far as I’m concerned, that is pure conjecture on their part.
What does happen in the real world is that there is a bug (somewhere far away. Unrelated to where the set based calculation is). Then, you try to run this somewhat large program and the non-determinism is messing up your ability to debug the unrelated bug, through spooky action at a distance.
You can have a loop that iterates a set and then calls thousands of SLOC below it that have nothing to do with sets, and now depending on the phase of the moon it may or may not trigger the chain of events that causes the bug to occur.
I thought all devs here would immediately know what I’m talking about when I started. But I guess it’s not as obvious as I thought.
In a simple cost-benefit analysis, The str hash non determinism has benefit (complexity attack resistance) and it can be turned off. The None hash non determinism has no benefit and can’t be turned off.
Non determinism for no benefit should be understood as bad tradeoff to make. All other things being equal, deterministic behavior is better. I hope you agree with that …
And in any case, None being hashed deterministically or not does not impose any constraints whatsoever on the rest of the runtime, its behaviors, future changes, future requirements, including changes to set in particular. If you want to make set into a maximally hostile agent of chaos, you can still do it, the hash of None will not stop you, not any more than the identity hashes of all the ints in the world would
Given I had these notions from the beginning, maybe you can see why the reaction from devs (and several other users on this forum) was completely bewildering to me?
So you’re just repeating what I said. If it doesn’t need determinism, it’s not a bug.
Again, as I said, it’s helpful in that situation to be deterministic.
It’s obvious enough to me. But you go from there to conclusions that I don’t think follow.
Incorrect. It has the benefit of not making None a special case compared to the rule that an object’s hashes is its address.
Well, I guess so, but all other things aren’t equal.
Well, I guess so. If you believed that it’s obvious that this change you proposed was self-evidently worth doing and had no significant downsides, I can see that you might be confused to find that others thought differently. I’m still surprised that you didn’t consider that maybe the core devs might have a better idea of the costs than you do, though…
But hopefully you’re now less bewildered, and can see that others do indeed have different opinions, including the people who are responsible for deciding whether to implement your proposal.
Fails if there are multiple falsey items in the collection, fails if there are any truthy items which can’t be compared with others, and in general, cannot be depended on to guarantee a reproducible order for arbitrary objects. TBH if I wanted something that coped with “strings plus None”, I’d probably do key=lambda x: (x is not None, x) to get None to sort before everything else; but to come closer to the Py2 notion of the way that objects compare, it would probably have to be done with a cmp function rather than a nice simple key (I can’t remember the rules but I believe it’s something like “-1 if x < y else 1 if y > x else 0, but if that throws, compare the names of the types instead”).
Not sure about other OSs (though Windows seems to not use ASLR by default).
That should help with reproducibility more than just a fixed hash(None).
You’ll also need to explicitly seed or fix any other sources of randomness you’re using (random.seed, numpy.random.default_rng, etc.), of course. Not sure if Python should provide a way to make this easier: since the settings need to be applied before Python starts, perhaps a launcher (like Jupyter) would be a better home for a “give me reproducibility where possible” setting.