Strange behaviour at key substitution in a dictionary

Viewed 63

I have a dictionary with keys as single characters. I want to substitute the upper-cased characters with doubled versions of them.

For example, I have this structure:

x = 'AbCDEfGH'
a = dict(zip(list(x), range(len(x))))
print(a)

which creates this dictionary:

{'A': 0, 'b': 1, 'C': 2, 'D': 3, 'E': 4, 'f': 5, 'G': 6, 'H': 7}

The values don't matter, so I just use some integers. What I want is to substitute the upper-cased keys with double characters, so that I get this:

{'AA': 0, 'b': 1, 'CC': 2, 'DD': 3, 'EE': 4, 'f': 5, 'GG': 6, 'HH': 7}

So, I tried the following in-place substitution:

for k, v in a.items():
    if k.isupper():
        a[k+k] = a.pop(k)
print(a)

But this, strangely, results in:

{'b': 1, 'E': 4, 'f': 5, 'G': 6, 'CCCCCCCCCCCCCCCC': 2, 'DDDDDDDDDDDDDDDD': 3, 'HHHHHHHHHHHHHHHH': 7, 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA': 0}

Even stranger, if I set all keys to upper-case:

y = 'ABCDEFGH'
a = dict(zip(list(y), range(len(y))))
for k, v in a.items():
    if k.isupper():
        a[k+k] = a.pop(k)
print(a)

it yields:

{'D': 3, 'E': 4, 'F': 5, 'CCCCCCCC': 2, 'GGGGGGGG': 6, 'HHHHHHHH': 7, 'AAAAAAAAAAAAAAAA': 0, 'BBBBBBBBBBBBBBBB': 1}
  • What is happening? I see the keys are repeating in magnitudes of 2. But, why?
  • I don't really care about the order of the items, but I see some aren't even being changed.
  • Is there any other way to substitute the keys the way I intend to?
1 Answers

.items() returns a live view of the underlying dict contents. Mutating the dict while iterating it causes unpredictable effects, usually leading to some keys being processed more than once (thus some keys doubling multiple times), while others aren't processed at all. Python tries to defend you from this by raising a RuntimeError if the dict changes size during iteration, but your code is keeping a constant size at the time of the check (when the next item is requested from the iterator), so Python's cheap length check doesn't save you.

The minimal fix is to make your loop run over a snapshot of the items:

for k, v in tuple(a.items()):

A simpler solution is a dict comprehension though:

a = {k*2 if k.isupper() else k: v for k, v in a.items()}

That builds a whole new dict with the doubled keys before reassigning a, so no mutation issues occur. You could build a in one fell swoop for that matter, just by doing:

 a = {let*2 if let.isupper() else let: i for i, let in enumerate(x)}

No need to listify x (strings already iterate by character) and enumerate can take care of numbering the values for you without needing zip, range or len at all.

Related