rabin utam
rabin utam

Reputation: 14800

Rename a dictionary key

Is there a way to rename a dictionary key, without reassigning its value to a new name and removing the old name key; and without iterating through dict key/value?

In case of OrderedDict do the same, while keeping that key's position.

Upvotes: 741

Views: 556364

Answers (17)

Artem Skoretskiy
Artem Skoretskiy

Reputation: 960

Reusable function to rename keys on fly:

def rename_dict_keys(data, **old_to_new_names):
    return {old_to_new_names.get(k, k): v for k, v in data.items()}

Usage:

>>> data = {"old": True}
>>> rename_dict_keys(data, old="new")
{"new": True}

Benefits:

  • do not change initial dict
  • preserve older of elements
  • easy to define mapping when used

Upvotes: 0

Shay
Shay

Reputation: 81

A one-liner solution, for the case where the renamed keys are provided as a dictionary:

# "to_rename" is the original dictionary and "key_map" is a dictionary  
# which maps zero or more of the original keys into new keys:
renamed_dict = {key_map.get(k, k): v for k, v in to_rename.items()}

Upvotes: 0

Chase
Chase

Reputation: 383

In my case, I had a function call returning a dict, which had a key I was hoping to rename in a single line, so none of these worked for me. Starting in python 3.8, you can use the walrus operator to keep it to one line if you are not looking for an inplace operation and the dict is not yet defined.

old_dict = get_dict()  
# old_dict = {'a': 1, 'b': 2, 'c': 3}

new_dict = {'new1': (x := get_dict()).pop('b'), **x}  
# new_dict = {'a': 1, 'new1': 2, 'c': 3}

Upvotes: 0

Zuzu Corneliu
Zuzu Corneliu

Reputation: 1703

For the keeping of order case (the other one is trivial, remove old and add new one): I was not satisfied with the ordered-dictionary needing reconstruction (at least partially), obviously for efficiency reasons, so I've put together a class (OrderedDictX) that extends OrderedDict and allows you to do key changes efficiently, i.e. in O(1) complexity. The implementation can also be adjusted for the now-ordered built-in dict class.

It uses 2 extra dictionaries to remap the changed keys ("external" - i.e. as they appear externally to the user) to the ones in the underlying OrderedDict ("internal") - the dictionaries will only hold keys that were changed so as long as no key changing is done they will be empty.

Performance measurements:

import timeit
import random

# Efficiency tests
from collections import MutableMapping

class OrderedDictRaymond(dict, MutableMapping):
    def __init__(self, *args, **kwds):
        if len(args) > 1:
            raise TypeError('expected at 1 argument, got %d', len(args))
        if not hasattr(self, '_keys'):
            self._keys = []
        self.update(*args, **kwds)

    def rename(self,key,new_key):
        ind = self._keys.index(key)  #get the index of old key, O(N) operation
        self._keys[ind] = new_key    #replace old key with new key in self._keys
        self[new_key] = self[key]    #add the new key, this is added at the end of self._keys
        self._keys.pop(-1)           #pop the last item in self._keys
        dict.__delitem__(self, key)

    def clear(self):
        del self._keys[:]
        dict.clear(self)

    def __setitem__(self, key, value):
        if key not in self:
            self._keys.append(key)
        dict.__setitem__(self, key, value)

    def __delitem__(self, key):
        dict.__delitem__(self, key)
        self._keys.remove(key)

    def __iter__(self):
        return iter(self._keys)

    def __reversed__(self):
        return reversed(self._keys)

    def popitem(self):
        if not self:
            raise KeyError
        key = self._keys.pop()
        value = dict.pop(self, key)
        return key, value

    def __reduce__(self):
        items = [[k, self[k]] for k in self]
        inst_dict = vars(self).copy()
        inst_dict.pop('_keys', None)
        return (self.__class__, (items,), inst_dict)

    setdefault = MutableMapping.setdefault
    update = MutableMapping.update
    pop = MutableMapping.pop
    keys = MutableMapping.keys
    values = MutableMapping.values
    items = MutableMapping.items

    def __repr__(self):
        pairs = ', '.join(map('%r: %r'.__mod__, self.items()))
        return '%s({%s})' % (self.__class__.__name__, pairs)

    def copy(self):
        return self.__class__(self)

    @classmethod
    def fromkeys(cls, iterable, value=None):
        d = cls()
        for key in iterable:
            d[key] = value
        return d

class obj_container:
    def __init__(self, obj) -> None:
        self.obj = obj

def change_key_splice(container, k_old, k_new):
    od = container.obj
    container.obj = OrderedDict((k_new if k == k_old else k, v) for k, v in od.items())

def change_key_raymond(container, k_old, k_new):
    od = container.obj
    od.rename(k_old, k_new)

def change_key_odx(container, k_old, k_new):
    odx = container.obj
    odx.change_key(k_old, k_new)

NUM_ITEMS = 20000
od_splice = OrderedDict([(x, x) for x in range(NUM_ITEMS)])
od_raymond = OrderedDictRaymond(od_splice.items())
odx = OrderedDictX(od_splice.items())
od_splice, od_raymond, odx = [obj_container(d) for d in [od_splice, od_raymond, odx]]
assert odx.obj == od_splice.obj
assert odx.obj == od_raymond.obj
# Pick randomly half of the keys to change
keys_to_change = random.sample(range(NUM_ITEMS), NUM_ITEMS//2)
print(f'OrderedDictX: {timeit.timeit(lambda: [change_key_odx(odx, k, k+NUM_ITEMS) for k in keys_to_change], number=1)}')
print(f'OrderedDictRaymond: {timeit.timeit(lambda: [change_key_raymond(od_raymond, k, k+NUM_ITEMS) for k in keys_to_change], number=1)}')
print(f'Splice: {timeit.timeit(lambda: [change_key_splice(od_splice, k, k+NUM_ITEMS) for k in keys_to_change], number=1)}')
assert odx.obj == od_splice.obj
assert odx.obj == od_raymond.obj

And results:

OrderedDictX: 0.06587849999999995
OrderedDictRaymond: 1.1131364
Splice: 1165.2614647

As expected, the splicing method is extremely slow (didn't expect it to be that much slower either though) and uses a lot of memory, and the O(N) solution of @Ashwini Chaudhary (bug-fixed though, del also needed) is also slower, 17X times in this example.

Of course, this solution being O(1), compared to the O(N) OrderedDictRaymond the time difference becomes much more apparent as the dictionary size increases, e.g. for 5 times more elements (100000), the O(N) is now 100X slower:

NUM_ITEMS = 100000
OrderedDictX: 0.3636919999999999
OrderedDictRaymond: 36.3963971

Here's the code, please comment if you see issues or have improvements to propose as this might still be error-prone.

from collections import OrderedDict


class OrderedDictX(OrderedDict):
    def __init__(self, *args, **kwargs):
        # Mappings from new->old (ext2int), old->new (int2ext).
        # Only the keys that are changed (internal key doesn't match what the user sees) are contained.
        self._keys_ext2int = OrderedDict()
        self._keys_int2ext = OrderedDict()
        self.update(*args, **kwargs)

    def change_key(self, k_old, k_new):
        # Validate that the old key is part of the dict
        if not self.__contains__(k_old):
            raise Exception(f'Cannot rename key {k_old} to {k_new}: {k_old} not existing in dict')

        # Return if no changing is actually to be done
        if len(OrderedDict.fromkeys([k_old, k_new])) == 1:
            return

        # Validate that the new key would not conflict with another one
        if self.__contains__(k_new):
            raise Exception(f'Cannot rename key {k_old} to {k_new}: {k_new} already in dict')

        # Change the key using internal dicts mechanism
        if k_old in self._keys_ext2int:
            # Revert change temporarily
            k_old_int = self._keys_ext2int[k_old]
            del self._keys_ext2int[k_old]
            k_old = k_old_int
            # Check if new key matches the internal key
            if len(OrderedDict.fromkeys([k_old, k_new])) == 1:
                del self._keys_int2ext[k_old]
                return

        # Finalize key change
        self._keys_ext2int[k_new] = k_old
        self._keys_int2ext[k_old] = k_new

    def __contains__(self, k) -> bool:
        if k in self._keys_ext2int:
            return True
        if not super().__contains__(k):
            return False
        return k not in self._keys_int2ext

    def __getitem__(self, k):
        if not self.__contains__(k):
            # Intentionally raise KeyError in ext2int
            return self._keys_ext2int[k]
        return super().__getitem__(self._keys_ext2int.get(k, k))

    def __setitem__(self, k, v):
        if k in self._keys_ext2int:
            return super().__setitem__(self._keys_ext2int[k], v)
        # If the key exists in the internal state but was renamed to a k_ext,
        # employ this trick: make it such that it appears as if k_ext has also been renamed to k
        if k in self._keys_int2ext:
            k_ext = self._keys_int2ext[k]
            self._keys_ext2int[k] = k_ext
            k = k_ext
        return super().__setitem__(k, v)

    def __delitem__(self, k):
        if not self.__contains__(k):
            # Intentionally raise KeyError in ext2int
            del self._keys_ext2int[k]
        if k in self._keys_ext2int:
            k_int = self._keys_ext2int[k]
            del self._keys_ext2int[k]
            del self._keys_int2ext[k_int]
            k = k_int
        return super().__delitem__(k)

    def __iter__(self):
        yield from self.keys()

    def __reversed__(self):
        for k in reversed(super().keys()):
            yield self._keys_int2ext.get(k, k)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, dict):
            return False
        if len(self) != len(other):
            return False
        for (k, v), (k_other, v_other) in zip(self.items(), other.items()):
            if k != k_other or v != v_other:
                return False
        return True

    def update(self, *args, **kwargs):
        for k, v in OrderedDict(*args, **kwargs).items():
            self.__setitem__(k, v)

    def popitem(self, last=True) -> tuple:
        if not last:
            k = next(iter(self.keys()))
        else:
            k = next(iter(reversed(self.keys())))
        v = self.__getitem__(k)
        self.__delitem__(k)
        return k, v

    class OrderedDictXKeysView:
        def __init__(self, odx: 'OrderedDictX', orig_keys):
            self._odx = odx
            self._orig_keys = orig_keys

        def __iter__(self):
            for k in self._orig_keys:
                yield self._odx._keys_int2ext.get(k, k)

        def __reversed__(self):
            for k in reversed(self._orig_keys):
                yield self._odx._keys_int2ext.get(k, k)

    class OrderedDictXItemsView:
        def __init__(self, odx: 'OrderedDictX', orig_items):
            self._odx = odx
            self._orig_items = orig_items

        def __iter__(self):
            for k, v in self._orig_items:
                yield self._odx._keys_int2ext.get(k, k), v

        def __reversed__(self):
            for k, v in reversed(self._orig_items):
                yield self._odx._keys_int2ext.get(k, k), v

    def keys(self):
        return self.OrderedDictXKeysView(self, super().keys())

    def items(self):
        return self.OrderedDictXItemsView(self, super().items())

    def copy(self):
        return OrderedDictX(self.items())    


# FIXME: move this to pytest
if __name__ == '__main__':
    MAX = 25
    items = [(i+1, i+1) for i in range(MAX)]
    keys = [i[0] for i in items]
    d = OrderedDictX(items)

    # keys() before change
    print(list(d.items()))
    assert list(d.keys()) == keys
    # __contains__ before change
    assert 1 in d
    # __getitem__ before change
    assert d[1] == 1
    # __setitem__ before change
    d[1] = 100
    assert d[1] == 100
    d[1] = 1
    assert d[1] == 1
    # __delitem__ before change
    assert MAX in d
    del d[MAX]
    assert MAX not in d
    d[MAX] = MAX
    assert MAX in d
    print('== Tests before key change finished ==')

    # change_key and __contains__
    assert MAX-1 in d
    assert MAX*2 not in d
    d.change_key(MAX-1, MAX*2)
    assert MAX-1 not in d
    assert MAX*2 in d
    # items() and keys()
    items[MAX-2] = (MAX*2, MAX-1)
    keys[MAX-2] = MAX*2
    assert list(d.items()) == items
    assert list(d.keys()) == keys
    print(list(d.items()))
    # __getitem__
    assert d[MAX*2] == MAX-1
    # __setitem__
    d[MAX*2] = MAX*3
    items[MAX-2] = (MAX*2, MAX*3)
    keys[MAX-2] = MAX*2
    assert list(d.items()) == items
    assert list(d.keys()) == keys
    # __delitem__
    del d[MAX]
    items = items[:-1]
    keys = keys[:-1]
    assert list(d.items()) == items
    assert list(d.keys()) == keys
    d[MAX] = MAX
    items.append((MAX, MAX))
    keys.append(MAX)
    # __iter__
    assert list(d) == keys
    # __reversed__
    print(list(reversed(d.items())))
    assert list(reversed(d)) == list(reversed(keys))
    assert list(reversed(d.keys())) == list(reversed(keys))
    assert list(reversed(d.items())) == list(reversed(items))
    # pop_item()
    assert d.popitem() == (MAX, MAX)
    assert d.popitem() == (MAX*2, MAX*3)
    items = items[:-2]
    keys = keys[:-2]
    assert list(d.items()) == items
    assert list(d.keys()) == keys
    # update()
    d.update({1: 1000, MAX-2: MAX*4})
    items[0] = (1, 1000)
    items[MAX-3] = (MAX-2, MAX*4)
    assert list(d.items()) == items
    assert list(d.keys()) == keys
    # move_to_end()
    d.move_to_end(1)
    items = items[1:] + [items[0]]
    keys = keys[1:] + [keys[0]]
    assert list(d.items()) == items
    assert list(d.keys()) == keys
    # __eq__
    d.change_key(1, 2000)
    other_d = OrderedDictX(d.items())
    assert d == other_d
    assert other_d == d

Upvotes: 0

henrry
henrry

Reputation: 623

You can use below code:

OldDict={'a':'v1', 'b':'v2', 'c':'v3'}

OldKey=['a','b','c']
NewKey=['A','B','C']

def DictKeyChanger(dict,OldKey,NewKey):
    ListAllKey=list(dict.keys())
    for x in range(0,len(NewKey)):
        dict[NewKey[x]]=dict[OldKey[x]] if OldKey[x] in ListAllKey else None
    for x in ListAllKey:
        dict.pop(x)
    return dict

NewDict=DictKeyChanger(OldDict,OldKey,NewKey)
print(NewDict)#===>>{'A': 'v1', 'B': 'v2', 'C': 'v3'}

Notes:

  1. The length of list OldKey and list NewKey must be equal.
  2. The length of the list OldKey must be equal to the listNewKey, if the key does not exist in the OldKey, put 'noexis' instead as shown as.

Example:

OldDict={'a':'v1', 'b':'v2', 'c':'v3'}
OldKey=['a','b','c','noexis','noexis']
NewKey=['A','B','C','D','E']
NewDict=DictKeyChanger(OldDict,OldKey,NewKey)
print(NewDict)#===>>{'A': 'v1', 'B': 'v2', 'C': 'v3', 'D': None, 'E': None}

Upvotes: -1

wim
wim

Reputation: 362707

For a regular dict, you can use:

mydict[k_new] = mydict.pop(k_old)

This will move the item to the end of the dict, unless k_new was already existing in which case it will overwrite the value in-place.

For a Python 3.7+ dict where you additionally want to preserve the ordering, the simplest is to rebuild an entirely new instance. For example, renaming key 2 to 'two':

>>> d = {0:0, 1:1, 2:2, 3:3}
>>> {"two" if k == 2 else k:v for k,v in d.items()}
{0: 0, 1: 1, 'two': 2, 3: 3}

The same is true for an OrderedDict, where you can't use dict comprehension syntax, but you can use a generator expression:

OrderedDict((k_new if k == k_old else k, v) for k, v in od.items())

Modifying the key itself, as the question asks for, is impractical because keys are hashable which usually implies they're immutable and can't be modified.

Upvotes: 1252

Lokesh Sanapalli
Lokesh Sanapalli

Reputation: 1034

I came up with this function which does not mutate the original dictionary. This function also supports list of dictionaries too.

import functools
from typing import Union, Dict, List


def rename_dict_keys(
    data: Union[Dict, List[Dict]], old_key: str, new_key: str
):
    """
    This function renames dictionary keys

    :param data:
    :param old_key:
    :param new_key:
    :return: Union[Dict, List[Dict]]
    """
    if isinstance(data, dict):
        res = {k: v for k, v in data.items() if k != old_key}
        try:
            res[new_key] = data[old_key]
        except KeyError:
            raise KeyError(
                "cannot rename key as old key '%s' is not present in data"
                % old_key
            )
        return res
    elif isinstance(data, list):
        return list(
            map(
                functools.partial(
                    rename_dict_keys, old_key=old_key, new_key=new_key
                ),
                data,
            )
        )
    raise ValueError("expected type List[Dict] or Dict got '%s' for data" % type(data))

Upvotes: 1

Shishir
Shishir

Reputation: 337

Suppose you want to rename key k3 to k4:

temp_dict = {'k1':'v1', 'k2':'v2', 'k3':'v3'}
temp_dict['k4']= temp_dict.pop('k3')

Upvotes: 6

Ikbel
Ikbel

Reputation: 2203

In case of renaming all dictionary keys:

target_dict = {'k1':'v1', 'k2':'v2', 'k3':'v3'}
new_keys = ['k4','k5','k6']

for key,n_key in zip(target_dict.keys(), new_keys):
    target_dict[n_key] = target_dict.pop(key)

Upvotes: 24

skullgoblet1089
skullgoblet1089

Reputation: 614

@helloswift123 I like your function. Here is a modification to rename multiple keys in a single call:

def rename(d, keymap):
    """
    :param d: old dict
    :type d: dict
    :param keymap: [{:keys from-keys :values to-keys} keymap]
    :returns: new dict
    :rtype: dict
    """
    new_dict = {}
    for key, value in zip(d.keys(), d.values()):
        new_key = keymap.get(key, key)
        new_dict[new_key] = d[key]
    return new_dict

Upvotes: 1

KeithWM
KeithWM

Reputation: 1345

In Python 3.6 (onwards?) I would go for the following one-liner

test = {'a': 1, 'old': 2, 'c': 3}
old_k = 'old'
new_k = 'new'
new_v = 4  # optional

print(dict((new_k, new_v) if k == old_k else (k, v) for k, v in test.items()))

which produces

{'a': 1, 'new': 4, 'c': 3}

May be worth noting that without the print statement the ipython console/jupyter notebook present the dictionary in an order of their choosing...

Upvotes: 4

Sirmione
Sirmione

Reputation: 311

In case someone wants to rename all the keys at once providing a list with the new names:

def rename_keys(dict_, new_keys):
    """
     new_keys: type List(), must match length of dict_
    """

    # dict_ = {oldK: value}
    # d1={oldK:newK,} maps old keys to the new ones:  
    d1 = dict( zip( list(dict_.keys()), new_keys) )

          # d1{oldK} == new_key 
    return {d1[oldK]: value for oldK, value in dict_.items()}

Upvotes: 3

excyberlabber
excyberlabber

Reputation: 649

I am using @wim 's answer above, with dict.pop() when renaming keys, but I found a gotcha. Cycling through the dict to change the keys, without separating the list of old keys completely from the dict instance, resulted in cycling new, changed keys into the loop, and missing some existing keys.

To start with, I did it this way:

for current_key in my_dict:
    new_key = current_key.replace(':','_')
    fixed_metadata[new_key] = fixed_metadata.pop(current_key)

I found that cycling through the dict in this way, the dictionary kept finding keys even when it shouldn't, i.e., the new keys, the ones I had changed! I needed to separate the instances completely from each other to (a) avoid finding my own changed keys in the for loop, and (b) find some keys that were not being found within the loop for some reason.

I am doing this now:

current_keys = list(my_dict.keys())
for current_key in current_keys:
    and so on...

Converting the my_dict.keys() to a list was necessary to get free of the reference to the changing dict. Just using my_dict.keys() kept me tied to the original instance, with the strange side effects.

Upvotes: 1

helloswift123
helloswift123

Reputation: 163

Other answers are pretty good.But in python3.6, regular dict also has order. So it's hard to keep key's position in normal case.

def rename(old_dict,old_name,new_name):
    new_dict = {}
    for key,value in zip(old_dict.keys(),old_dict.values()):
        new_key = key if key != old_name else new_name
        new_dict[new_key] = old_dict[key]
    return new_dict

Upvotes: 4

Uri Goren
Uri Goren

Reputation: 13692

A few people before me mentioned the .pop trick to delete and create a key in a one-liner.

I personally find the more explicit implementation more readable:

d = {'a': 1, 'b': 2}
v = d['b']
del d['b']
d['c'] = v

The code above returns {'a': 1, 'c': 2}

Upvotes: 11

Using a check for newkey!=oldkey, this way you can do:

if newkey!=oldkey:  
    dictionary[newkey] = dictionary[oldkey]
    del dictionary[oldkey]

Upvotes: 39

Ashwini Chaudhary
Ashwini Chaudhary

Reputation: 250951

You can use this OrderedDict recipe written by Raymond Hettinger and modify it to add a rename method, but this is going to be a O(N) in complexity:

def rename(self,key,new_key):
    ind = self._keys.index(key)  #get the index of old key, O(N) operation
    self._keys[ind] = new_key    #replace old key with new key in self._keys
    self[new_key] = self[key]    #add the new key, this is added at the end of self._keys
    self._keys.pop(-1)           #pop the last item in self._keys

Example:

dic = OrderedDict((("a",1),("b",2),("c",3)))
print dic
dic.rename("a","foo")
dic.rename("b","bar")
dic["d"] = 5
dic.rename("d","spam")
for k,v in  dic.items():
    print k,v

output:

OrderedDict({'a': 1, 'b': 2, 'c': 3})
foo 1
bar 2
c 3
spam 5

Upvotes: 12

Related Questions