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.

Asked By: uv_

||

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/

Answered By: ddejohn
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.