Search code examples
pythonsentinel

How to create a falsy sentinel object in Python?


I have a function which is designed to be called by passing in one of to keyword arguments. I'm using a sentinel object as default value, so that I can make sure no one just calls func() without any arguments, which is a clear logical error. It is ok to call the function by passing None as a value for one of the arguments, in those cases it just doesn't do any processing.

NO_VALUE = object()


def func(*, arg1 = NO_VALUE, arg2 = NO_VALUE):
    if arg1 is NO_VALUE and arg2 is NO_VALUE:
        raise ValueError("Pass in one of `arg1` or arg2`.")

    if arg1 is not NO_VALUE and arg1:
        # Do something with a truthy `arg1` value.
    if arg2 is not NO_VALUE and arg2:
        # Do something with a truthy `arg2` value.

Could I somehow easily make NO_VALUE be falsy, so that I could simplify the if arg1 is not NO_VALUE and arg1 and if arg2 is not NO_VALUE and arg2 to just if arg1 and if arg2 respectively?

I tried making NO_VALUE an empty tuple () but it seems that the id() of an empty tuple is always(?) same as the id() of any other empty tuple. I also don't want to make NO_VALUE e.g. an empty list object, since then I'd get linter warnings about using a mutable default value.


Solution

  • In any case: for a Python object to be "Falsy" it can be an instance of a class which implements __bool__ and returns False when that is called:

    class FalsySentinel:
        def  __bool__(self):
            return False
    
    
    NO_VALUE = FalsySentinel()
    
    
    def func(*, arg1 = NO_VALUE, arg2 = NO_VALUE):
        if arg1 is NO_VALUE and arg2 is NO_VALUE:
            raise ValueError("Pass in one of `arg1` or arg2`.")
    
        if arg1:
            # Do something with a truthy `arg1` value.
        if arg2:
            # Do something with a truthy `arg2` value.
    

    There are other ways to produce falsy objects, like objects that implement __len__ and return 0 on it - but this is the most explicit and straightforward way.