Say no to more optional arguments

They're often smelly and biased towards historical code

Often you want to pass in an extra variable to an existing function

def my_func(a):
  ...

so it supports a new use. You usually want the old use to remain the same, so you add a optional argument with a default value. Often, in Python at least, this default is None.

def my_func(a, b=None):
  ...

So after adding this, you would have two (or more) call sites.

# Original call site
my_func(a=arg_original)

# New call site
my_func(a=arg_new_1, b=arg_new_2)

and quite likely inside the function there will be at least one new block of code that modifies its behaviour in the new case.

def my_func(a, b=None):
  ...
  if b is not None:
    ...

This is a run-time check, even though you know, at write-time, that the case you want this to run is in the new call site. Having write-time knowledge that you convert to a run-time check is often needlessly increasing number of branches in the code1. I suspect it is a response to personal bias against touching existing code, and it biases the code itself: the original use case is clearer than than the new one. All this, I suspect, is likely to make further changes to the code more difficult.

One small improvement is to not have a default value in the parameter list, so the function looks like

def my_func(a, b):
  ...
  if b is not None:
    ...

and change the original call site to explicitly pass None.

# Original call site
my_func(a=arg_original, b=None)

However, exactly because you can pass None, and the function handles this case separately, b is still a de-facto optional argument to the function.

What to do about this depends on the actual code in question. However, often there is code in the function that can be factored out to functions called from outside of the function in question, ideally each returning meaningful values.

# Original call site
my_func(a=arg_original, b=foo())

# New call site
my_func(a=arg_new, b=bar())

so the code of my_func no longer has a special case of handling b being None.

In some cases, if you're working in a language that allows lambda functions, it might be clearer to extract out a lambda function that you pass in.

# Original call site
my_func(a=arg_original, b=lambda x: foo(x))

# New call site
my_func(a=arg_new, b=lambda x: bar(x))

And of course, if you're working in an object oriented language, it might be appropriate to go the whole hog and pass in an object.

# Original call site
my_func(a=arg_original, b=new Foo())

# New call site
my_func(a=arg_new, b=new Bar())

These changes have benefits that they keeps the reponsibility of the original function small, chances are it has limited the number of branches, and it does not bias the code base: the new call site is as clear and deliberate as the original call site.


1 This statement only has relevance if number of branches is seen as something to be deliberatly avoided, a view which may be oversimplisitc, and in some cases, counter-productive. I am wary of following it too strictly.