Search code examples
pythonpython-2.7typesduck-typingdynamic-typing

How to ensure solid contracts without strong typing?


get_min_length() takes an argument that must match the possible return values of get_pkt_type():

def get_pkt_type(some_val):
    """Determine the type of an XCP packet.

    :return:
        'CMD' if "Command" packet,
        'RES' if "Command Response" packet,
        'ERR' if "Error" packet,
        'CMD/RES' if uncertain whether a CONNECT CMD or a
            RES packet, as their frame bytes can look identical.
    :rtype: str
    """
    if something:
        return 'CMD'
    elif something_else2:
        return 'RES'
    elif something_else3:
        return 'ERR'
    elif something_else4:
        return 'CMD/RES'

def get_min_length(packet_type):
    if packet_type in ['CMD', 'RES']:
        return 4
    elif packet_type in ['ERR', 'CMD/RES']:
        return 6

packet_type = get_pkt_type(some_val)
length = get_min_length(packet_type)

How do I ensure that, if a programmer adds a new packet type return value to get_pkt_type(), that he also doesn't forget to add the value to get_min_length(). In a strong-typed language packet_type would be a defined type that is returned and passed, so I would have safety that way.


Solution

  • Generally, in Python, if you have an extensible set of values, it makes sense to make a shallow inheritance hierarchy instead. This is less prone to forgetfulness. Enums are better for fixed sets of values.

    That said, the first thing you should be doing is trailing

    raise ValueError("Unexpected enum value")
    

    to your functions.

    Another thing you might consider is using dictionaries to represent such mappings:

    pkt_lengths = {
        'CMD': 4,
        'RES': 4,
        'ERR': 6,
        'CMD/RES': 6,
    }
    
    get_min_length = pkt_lengths.__getitem__
    

    You can then add a simple test

    packet_types = {'CMD', 'RES', 'ERR', 'CMD/RES'}
    assert not packet_types.symmetric_difference(pkt_lengths)
    

    If you're doing this a lot, build a function:

    def enum_mapper(enum, mapper):
        assert not enum.symmetric_difference(mapper)
        return mapper.__getitem__
    

    which lets you do

    get_min_length = enum_mapper(packet_types, pkt_lengths)
    

    and get checks at startup.

    Also, consider using a proper enum.Enum.