First, the context:I'm working with a student to build a web frontend for a star cluster simulation package called starlab. Typical starlab usage involves chaining sequences of commands together with Unix pipes. We want to store both the output and the set of commands (and arguments) used to generate the output in a database.The specific piece of the puzzle I'm asking about here is representing the commands. Each command has a different set of arguments, some of which take values and others don't. Additionally, some of the commands have arguments for which defaults aren't supplied, so they need to be included.Since the commands form a predefined, discrete set, it seemed appropriate to use an Enum. Currently, I keep the commands and their arguments separate (stored in the Run object as tuples). Typical usage would look something like:foo = Run()foo.set_creation_command(StarlabCreationCommand.makeking, **makeking_args)foo.set_transform_command(StarlabTransformationCommand.scale, **scale_args)...commands = foo.generate_command_list()My questions:Does this seem like a reasonable approach to this problem?Should I roll the argument handling into the StarlabCommand Enum class, rather than storing the arguments in a tuple/dictionary?Is there anything here that could be improved?The code:from enum import Enumclass StarlabCommand(Enum):    Enumerator class for starlab commands.    Each member takes three parameters:    1. A dictionary of required arguments (with default values),    2. A list of optional arguments that take a value, and    3. A list of optional arguments that don't take a value.    If there are parameters which are not, strictly speaking, required    (i.e., the underlying starlab command will execute without them being    supplied) but I want to make sure they get into the database, I will    include them in the required list. The most common example of this is    random seed for those commands that use one.        def __init__(self, required, with_value, without_value):        Initialize.        self.required = required        self.with_value = with_value        self.without_value = without_value    def build_command(self, **cmd_args):        Build a command list suitable for passing to subprocess.Run()        command_list = [self.name]        for arg, default in self.required.items():            val = cmd_args.get(arg, default)            command_list.extend(['-'+arg, val])        for arg in self.with_value:            val = cmd_args.get(arg, None)            if val is not None:                command_list.extend(['-'+arg, val])        for arg in self.without_value:            val = cmd_args.get(arg, False)            if val:                command_list.append('-'+arg)        return command_listclass StarlabCreationCommand(StarlabCommand):    Starlab cluster creation commands.    The required args dictionaries here don't include the number of stars or the    random seed, which are required of all these commands and are passed in the    same way in all cases.        makesphere = ({}, ['R'], list('ilouU'))    makecube = ({}, ['L'], list('ilou'))    makeplummer = ({}, ['m', 'r'], list('iRou'))    makeking = ({'w':5.0}, ['b'], list('iou'))    def __init__(self, required, with_value, without_value):        Initialize.        All creation methods require a number of stars, and I'm adding        random seed to the required list.                super().__init__(required, with_value, without_value)        self.required['n'] = 500        self.required['s'] = 123456789class StarlabTransformationCommand(StarlabCommand):    Starlab cluster transformation commands.    There are more than these, but these are the ones we're supporting for now.    Unlike the case of creation commands, not all of these require a random seed    (and in fact, scale uses the s flag for something else) so I can't just put    that into an __init__() method.        makemass = ({'e':-2.35, 'f':1, 's':123456789}, list('hlu'), ['i', 'm'])    makesecondary = ({'s':123456789}, list('flmMu'), list('iIqS'))    scale = ({}, list('eEmqr'), ['c', 's'])    makebinary = ({'s':123456789}, list('felou'), [])class StarlabIntegrationCommand(StarlabCommand):    Time integration    kira = ({'d':1, 's':123456789, 't':10},            list('bDefFgGhIkKlLnNqRTWXyzZ'),            list('aABEioOrSuUvx'))class ArchivedRun(Base):    Class for archiving Run objects via SQLAlchemy.    __tablename__ = runs    run_id = Column(Integer, primary_key=True)    creation_command_string = Column(String, length=80)    creation_command = Column(Enum(StarlabCreationCommand))    creation_command_args = Column(String, length=80)    random_seed = Column(Integer)    n_stars = Column(Integer)    creation_scale_1 = Column(Float)    creation_scale_2 = Column(Float)    def __repr__(self):        return <Run %d> % self.run_idclass Run(object):    Metadata for a cluster simulation.    def __init__(self, random_seed=None, nstars=500):        Initialize.        self.creation_command = (None, None)        self.transform_commands = []        self.integration_command = (None, None)        if random_seed is None:            self.random_seed = uuid.uuid4().time_low        else:            self.random_seed = random_seed        self.nstars = nstars    def set_creation_command(self, creation_command, **args):        Set the creation command.        self.creation_command = (creation_command, args)        self.nstars = args.get('n', self.nstars)        self.random_seed = args.get('s', self.random_seed)        self.creation_command[0].required['n'] = self.nstars        self.creation_command[0].required['s'] = self.random_seed    def add_transform_command(self, transform_command, **args):        Add a transformation command.        There are some special caveats here:        1. If we're making a cluster that includes binaries, both makesecondary        and makebinary are required, in that order.        2. If we're additionally using scale, it should happen between the        makesecondary and makebinary commands                if 's' in transform_command.required.keys():            transform_command.required['s'] = args.get('s', self.random_seed)        self.transform_commands.append((transform_command, args))    def set_integration_command(self, integration_command, **args):        Set the integration command and its arguments.        self.integration_command = (integration_command, args)    def generate_command_list(self):        Build the list of commands for execution by Popen.        all_commands = [self.creation_command]        all_commands.extend(self.transform_commands)        all_commands.append(self.integration_command)        command_list = [cmd[0].build_command(cmd[1]) for cmd in all_commands]        return command_list
Avoiding numbness with Python Enum
python;python 3.x;enum
Here is another view on your class structure, in an attempt to make it both more readable and more pythonic so to speak. Let us start of with a yUml diagram of your class structure (somewhat simplified):As stated my main issue, is that the actual commands are hidden within the class structure, and I would rather make them separate classes. And I would focus on making the various options clearer. One way to do this is to change into a structure like the following diagram:The changes I've done in this revised diagram are the following:Removed the dependency on Enum and made the different commands into full blown classes within the hierarchySimplified the names of the classes, just to keep the naming simpler. Since everything would be in the starlab package a full reference would still be something like starlab.MakeBinary(upper_mass_limit=3, ...) or if you moved the different steps into packages of their own, it could become starlab.Transform.Scale(...).Allowed each command to have a set of options related to it, which are instantiated at the node level into the actual commandsIn the Simulation class you could now also add type safety that you only add intermediate steps of the Transform class, and similar for Creation and Integration. And in these classes you also have method or validators ensuring proper sequencing of transformation steps or similar.Sprinkling __format__() all over the places to allow for code similar to the following:a_king = starlab.MakeKing(depth=5, particle_numbers=10000)print('Command: {0:cmd}\n\nHtml:\n {0:html}'.format(a_king)Which would result in something like:Command: makeking -d 5 -n 10000Html:<div class=Command Creation>    <label>MakeKing</label>    <div class=Option>        <label>Depth</label>        <input>5.0</input>        <input type=button> Remove option</input>    </div>    <div class=Option>        <label>Particle numbers</label>        <input>10000</input>        <input type=button> Remove option</input>    </div>    <div class=add_option>       <select>          <option>Depth</option>          <option>Upper mass limit</option>          <option>Lower mass limit</option>          <option>...</option>        </select>     </div></div>The html code is of course way off, and needs to be tailored to your liking. The gist of the idea is that the different class holds options and descriptions which are easily displayed in html code with proper identifiers and links so that you if you add an option it is immediately available on the web page as it automatically knows how to handle that kind of option. For more information on __format__ see PEP3101 in the section Controlling Formatting on a Per-Type Basis, or alternatively PEP 3101: Advanced String Formatting (for Python 2) or (for Python 3).At higher levels than the Option, one would propage the format down to the specific sub objects, and join the different parts apropriately. I.e. The Creation would allow for changing the creation command in the html output, whilst Transform would allow for adding and removal of steps (possibly), and so on. Example of class MakeKingThe following example is a simplistic implementation of part of the MakeKing class.class MakeKing(Creation):    Wrapper class for the makeking command, starlab v 4.4.1.See http://sns.ias.edu/~starlab/tools/auto/makekingdef __init__(self, *args):    super().__init__()    options.add(depth, -d, 5.0, float,         Specify number of particles [no default]  +        if unspecified assumes an input snapshot with masses.)    options.add(particle_number, -i, bool,        Number the particles sequentially [don't number])    ...    for name, value in args.items():        if name in options:           options[name].set(name, value)And this would be used in a Simulation like in my other answer, with the exception of using the basic print statement on the entire simulation to actually get the unix command ready for popen or html text ready for presentation and handling on a web page.Hope this is somewhat clear, and gives you some good ideas on an alternate approach to your issue at hand.