How to make a dict key deprecated?
To be honest I do not think there is something particularly wrong or an anti-pattern with your solution, except for the fact that my_func
has to duplicate each deprecated key with its replacement (see below).
You could even generalize it a bit (in case you will decide to deprecate other keys):
class MyDict(dict):
old_keys_to_new_keys = {'some string': 'some object'}
def __getitem__(self, key):
if key in self.old_keys_to_new_keys:
msg = 'Please use the new key: `{}` instead of `{}`'.format(self.old_keys_to_new_keys[key], key)
warn(msg)
return super().__getitem__(key)
class MyObj:
pass
def my_func():
o = MyObj()
return MyDict({'some string' : o, 'some object': o, 'additional info': 'some other text'})
Then
>> my_func()['some string'])
UserWarning: Please use the new key: `some object` instead of `some string`
All you have to do now in order to "deprecate" more keys is update old_keys_to_new_keys
.
However,
note howmy_func
has to duplicate each deprecated key with its replacement. This violates the DRY principle and will clutter the code if and when you do need to deprecate more keys (and you will have to remember to update both MyDict.old_keys_to_new_keys
and my_func
). If I may quote Raymond Hettinger:
There must be a better way
This can be remedied with the following changes to __getitem__
:
def __getitem__(self, old_key):
if old_key in self.old_keys_to_new_keys:
new_key = self.old_keys_to_new_keys[old_key]
msg = 'Please use the new key: `{}` instead of `{}`'.format(new_key, old_key)
warn(msg)
self[old_key] = self[new_key] # be warned - this will cause infinite recursion if
# old_key == new_key but that should not really happen
# (unless you mess up old_keys_to_new_keys)
return super().__getitem__(old_key)
Then my_func
can only use the new keys:
def my_func():
o = MyObj()
return MyDict({'some object': o, 'additional info': 'some other text'})
The behavior is the same, any code using the deprecated keys will get the warning (and, of course, accessing the new keys work):
print(my_func()['some string'])
# UserWarning: Please use the new key: `some object` instead of `some string`
# <__main__.MyObj object at 0x000002FBFF4D73C8>
print(my_func()['some object'])
# <__main__.MyObj object at 0x000002C36FCA2F28>
As others have said, your current approach seems quite good already. The only potential caveat I see is that the MyDict
class centralizes all the knowledge about deprecated values. Depending on your use case, you might prefer to define what is and what is not deprecated at the point where it is defined instead. You could for example do something along these lines:
from warnings import warn
class MyDict(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._deprecated_keys = {}
def __getitem__(self, key):
if key in self._deprecated_keys:
new_key = self._deprecated_keys[key]
if new_key:
warn(f'Please use the new key: `{new_key}` instead of `{key}`.')
else:
warn(f'Deprecated key: `{key}`.')
return super().__getitem__(key)
# Option A
def put_deprecated(self, key, value, new_key=None):
self._deprecated_keys[key] = new_key
self[key] = value
# Option B
def put(self, key, value, deprecated_keys=None):
self[key] = value
for deprecated_key in (deprecated_keys or []):
self[deprecated_key] = value
self._deprecated_keys[deprecated_key] = key
my_dict = MyDict()
# Option A
my_dict['new_key'] = 'value'
my_dict.put_deprecated('old_key', 'value', new_key='new_key')
# Option B
my_dict.put('new_key', 'value', deprecated_keys=['old_key'])
my_dict['old_key']
# UserWarning: Please use the new key: `new_key` instead of `old_key`.
Option A requires repetition but allows for deprecated keys without replacement, while option B is more succinct. The advantage here is that defining new keys and deprecating old ones is done at the point where the key and value are assigned, instead of requiring changing MyDict
.