Home > OS >  Should keyword arguments with default values always be considered optional by the caller in Python?
Should keyword arguments with default values always be considered optional by the caller in Python?

Time:09-16

I'm trying to decide if raising an exception when the caller doesn't override a default keyword argument value is poor design. Put another way, should keyword arguments with default values always be considered optional by the caller?

My goal, I believe, is to extend functionality without breaking older code.

Image a design where:

  • Base class specifies a method signature: foo().
  • Child class needs to extend the functionality of foo() but this requires additional information.
  • So the child class overrides foo() and adds a keyword argument for capturing the additional information: foo(bar=None).

The problem is the child class' foo implementation cannot use the value bar=None. It needs the caller to provide a valid bar value because there's no known default that can work. (None is used in the method signature to avoid explicitly breaking inheritance.)

From a design perspective, is it poor form to raise an exception (e.g., TypeError) if the caller does not explicitly provide a bar value, such as child.foo(bar='some_value')?

Detailed Context

I'm designing a library that allows a remote system to be controlled via Python. The remote system is ultimately controlled via its command-line tools. The library attempts to abstract away details about the remote system (e.g., version, environment, operating system) so users can focus on simply controlling the system. It's essentially mapping Python operations to shell commands for controlling the remote system, then running those commands. The library will be used for automated black-box testing of remote systems.

Importantly, the library needs to work against multiple versions of the remote software (e.g., v1, v2, v3), and the command syntax can vary between versions (e.g., the command might require more arguments in one version than the other). I want to account for this while duplicating as little code as possible.

My proposed solution is to begin with a class containing all the commands available in v1. I'll then create a subclass for commands in v2, overriding only methods for commands whose syntax has changed since v1. The will allow users to simply add new arguments when needed (depending on the software version being tested) without breaking inheritance by fully changing the method signature.

For example (in the contrived example below), BaseCommands contains the commands available in v1. CommandsVersion2 overrides commands whose syntax has changed. CommandVersion2 overrides the start_system because in v2, more information is required (username) and raises an exception if the caller does not replace the default value.

Is raising the exception when the caller doesn't replace the default value poor design?


class BaseCommands:
  """ Builds shell commands to be run on the remote system. """
  
  @staticmethod
  def display_message(*, message:str)->str:
    """ Command for displaying a message on the remote system. """
    timestamp = datetime.now().strftime("%d-%m-%Y, %H:%M:%S")
    return f'config.sh -display \"[{timestamp}] {message}\"'


  @staticmethod
  def start_system()->str:
    """ Command for starting the remote system. """
    return 'config.sh -start'


class CommandsVersion2(BaseCommands):
  """ Defines commands available in software v2."""

  @staticmethod
  def start_system(*,username:str=None)->str:
    """ Software v2 requires specifying who is starting the system. """
    if username is None:
        raise TypeError('username is required in Software v2.')
    return f'config.sh -start -user \"{username}\"'

The library might be used in tests like this:

v1_tests.py

@test
def test_startup():
  cmd = BaseCommands.start_system()
  result = LinuxTerminal().run(cmd)
  assertTrue(result.success)

v2_tests.py

@test test_startup():
  cmd = CommandsVersion2.start_system(user
  result = LinuxTerminal().run(cmd)
  assertTrue(result.success)

CodePudding user response:

The purpose of having a default value is to make an argument optional.

So the answer is: yes, it's poor design to raise an exception when a keyword argument has a default value and isn't overridden.

In contrast, if there is no default value defined, then it's good design to detect that immediately and raise an Exception.

CodePudding user response:

You decide how your code should be called. Always using keyword arguments is reasonable. And having a function with a required argument is normal as well.

Allow me to add, though: your reason to do so requires prior knowledge of how one version changes to the next. Imagine v1 is totally different from v2: then sub classing makes little sense. Or v1 differs from v2 where other methods of handling both might be more optimal.

Another thing: None might be an inadequate sentinel to detect the missing argument. The caller might have actually used None.

  • Related