Setting numerical key values in a hash I now know the answer to this but I’m posting it here as a note to myself and in case it helps people.
Let's say you are looping through some entries and storing values in a hash…
{% set myHash = {} %}

{% for entry in entries %}
    {% set value  = entry.intro %}
    {% set myHash = myHash|merge({
        (entry.id) : { intro : value }
    }) %}
{% endfor %}

The entry.id is a variable so it needs to be wrapped in brackets as a dynamic key.
But when you {{ dump(myHash) }} it turns out the numeric keys have just become index numbers – [0] in the truncated example below…
array(10) {
  [0]=&gt;
  array(1) {
    [&quot;intro&quot;]=&gt;
    string(13) &quot;My intro text&quot;
…

But there is a straightforward answer…
The answer is to prepend a letter to the numerical key…
{% for entry in entries %}
    {% set eId   = &quot;e#{entry.id}&quot; %} {# interpolation* #}
    {% set value = entry.intro %}

    {% set myHash = myHash|merge({
        (eId) : { intro : value }
    }) %}
{% endfor %}

Now the hash stores the custom key as a string…
array(10) {
  [&quot;e1234&quot;]=&gt;
  array(1) {
    [&quot;intro&quot;]=&gt;
    string(13) &quot;My intro text&quot;
…


…which you can clean up as needed…
{% for key, properties in hash %}
    {% set key = key|replace('e', '') %}
    …
{% endfor %}

…or factor into lookups…
{% set testId    = 'e1234' %}
{% set testIntro = myHash[testId].intro %}
{# `testIntro`  == 'My intro text' #}

Apparently, it's a PHP thing.
*Interpolation is often preferable to concatenation &lt;-- see item #4.