Best practice to rename a method parameter in a deployed Python module
Question:
Say I maintain a Python module with some method foo()
:
def foo(BarArg=None, AnotherArg=False):
return True
But now I’m not satisfied with the PascalCase of my argument names, and would like to rename them as such:
def foo(bar_arg=None, another_arg=False):
...
How can I introduce this change without breaking existing client code?
I wouldn’t really want deprecation warnings (but maybe that’s the best practice), and also would very much would like to keep my function’s name…
For now, **kwargs
plus some input validation logic is the only solution that comes to mind, but it seems like the wrong direction.
Answers:
You can use a decorator factory to intercept any uses of the incorrect args:
def re_arg(kwarg_map):
def decorator(func):
def wrapped(*args, **kwargs):
new_kwargs = {}
for k, v in kwargs.items():
if k in kwarg_map:
print(f"DEPRECATION WARNING: keyword argument '{k}' is no longer valid. Use '{kwarg_map[k]}' instead.")
new_kwargs[kwarg_map.get(k, k)] = v
return func(*args, **new_kwargs)
return wrapped
return decorator
# change your kwarg names as desired, and pass the kwarg re-mapping to the decorator factory
@re_arg({"BarArg": "bar_arg", "AnotherArg": "another_arg"})
def foo(bar_arg=None, another_arg=False):
return True
Demo:
In [7]: foo(BarArg="hello")
DEPRECATION WARNING: keyword argument 'BarArg' is no longer valid. Use 'bar_arg' instead.
Out[7]: True
In [8]: foo(AnotherArg="hello")
DEPRECATION WARNING: keyword argument 'AnotherArg' is no longer valid. Use 'another_arg' instead.
Out[8]: True
In [9]: foo(x="hello") # still errors out on invalid kwargs
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [9], line 1
----> 1 foo(x="hello")
Cell In [4], line 9, in re_arg.<locals>.wrapped(**kwargs)
7 print(f"DEPRECATION WARNING: keyword argument '{k}' is no longer valid. Use '{kwarg_map[k]}' instead.")
8 new_kwargs[kwarg_map.get(k, k)] = v
----> 9 return func(**new_kwargs)
TypeError: foo() got an unexpected keyword argument 'x'
In [10]: foo(another_arg="hello") # no warning if you pass a correct arg (`bar_arg` has a default so it doesn't show up in `new_kwargs`.
Out[10]: True
In [11]: foo(BarArg="world", AnotherArg="hello")
DEPRECATION WARNING: keyword argument 'BarArg' is no longer valid. Use 'bar_arg' instead.
DEPRECATION WARNING: keyword argument 'AnotherArg' is no longer valid. Use 'another_arg' instead.
Out[11]: True
You could get super fancy and leave in the old kwargs alongside the new ones, inspect the signature, extract the old kwargs and build the kwarg_map
dynamically, but that’d be quite a bit more work for probably not much gain in my opinion, so I’ll "leave it as an exercise for the reader".
Another solution would be to simply add a new_foo
function, transfer the old foo
implementation over, and simply call new_foo
from foo
with the kwarg re-mapping shown above, and with a deprecation warning, but I think this is cleaner than having to maintain a bunch of stubs.
You may also want to check out the deprecation
library: https://pypi.org/project/deprecation/
Say I maintain a Python module with some method foo()
:
def foo(BarArg=None, AnotherArg=False):
return True
But now I’m not satisfied with the PascalCase of my argument names, and would like to rename them as such:
def foo(bar_arg=None, another_arg=False):
...
How can I introduce this change without breaking existing client code?
I wouldn’t really want deprecation warnings (but maybe that’s the best practice), and also would very much would like to keep my function’s name…
For now, **kwargs
plus some input validation logic is the only solution that comes to mind, but it seems like the wrong direction.
You can use a decorator factory to intercept any uses of the incorrect args:
def re_arg(kwarg_map):
def decorator(func):
def wrapped(*args, **kwargs):
new_kwargs = {}
for k, v in kwargs.items():
if k in kwarg_map:
print(f"DEPRECATION WARNING: keyword argument '{k}' is no longer valid. Use '{kwarg_map[k]}' instead.")
new_kwargs[kwarg_map.get(k, k)] = v
return func(*args, **new_kwargs)
return wrapped
return decorator
# change your kwarg names as desired, and pass the kwarg re-mapping to the decorator factory
@re_arg({"BarArg": "bar_arg", "AnotherArg": "another_arg"})
def foo(bar_arg=None, another_arg=False):
return True
Demo:
In [7]: foo(BarArg="hello")
DEPRECATION WARNING: keyword argument 'BarArg' is no longer valid. Use 'bar_arg' instead.
Out[7]: True
In [8]: foo(AnotherArg="hello")
DEPRECATION WARNING: keyword argument 'AnotherArg' is no longer valid. Use 'another_arg' instead.
Out[8]: True
In [9]: foo(x="hello") # still errors out on invalid kwargs
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In [9], line 1
----> 1 foo(x="hello")
Cell In [4], line 9, in re_arg.<locals>.wrapped(**kwargs)
7 print(f"DEPRECATION WARNING: keyword argument '{k}' is no longer valid. Use '{kwarg_map[k]}' instead.")
8 new_kwargs[kwarg_map.get(k, k)] = v
----> 9 return func(**new_kwargs)
TypeError: foo() got an unexpected keyword argument 'x'
In [10]: foo(another_arg="hello") # no warning if you pass a correct arg (`bar_arg` has a default so it doesn't show up in `new_kwargs`.
Out[10]: True
In [11]: foo(BarArg="world", AnotherArg="hello")
DEPRECATION WARNING: keyword argument 'BarArg' is no longer valid. Use 'bar_arg' instead.
DEPRECATION WARNING: keyword argument 'AnotherArg' is no longer valid. Use 'another_arg' instead.
Out[11]: True
You could get super fancy and leave in the old kwargs alongside the new ones, inspect the signature, extract the old kwargs and build the kwarg_map
dynamically, but that’d be quite a bit more work for probably not much gain in my opinion, so I’ll "leave it as an exercise for the reader".
Another solution would be to simply add a new_foo
function, transfer the old foo
implementation over, and simply call new_foo
from foo
with the kwarg re-mapping shown above, and with a deprecation warning, but I think this is cleaner than having to maintain a bunch of stubs.
You may also want to check out the deprecation
library: https://pypi.org/project/deprecation/