Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to force mutually exclusive function parameters in python?

Consider:

def foobar(*, foo, bar):
    if foo:
        print('foo', end="")
    if bar:
        print('bar', end="")
    if foo and bar:
        print('No bueno', end='')  # I want this to be impossible
    if not foo and not bar:
        print('No bueno', end='')  # I want this to be impossible
    print('')


foobar(foo='bar')  # I want to pass inspection
foobar(bar='foo')  # I want to pass inspection
foobar(foo='bar', bar='foo')  # I want to fail inspection
foobar()  # I want to fail inspection

Is there a way to set up a function so that way calling it only passes inspection when just one of foo or bar is being passed, without manually checking inside the function?

like image 907
Samuel Anderson Avatar asked Oct 31 '25 11:10

Samuel Anderson


2 Answers

Syntactically no. However it's relatively easy to do this using a decorator:

from functools import wraps

def mutually_exclusive(keyword, *keywords):
    keywords = (keyword,)+keywords
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            if sum(k in keywords for k in kwargs) != 1:
                raise TypeError('You must specify exactly one of {}'.format(', '.join(keywords)))
            return func(*args, **kwargs)
        return inner
    return wrapper

Used as:

>>> @mutually_exclusive('foo', 'bar')
... def foobar(*, foo=None, bar=None):
...     print(foo, bar)
... 
>>> foobar(foo=1)
1 None
>>> foobar(bar=1)
None 1
>>> foobar(bar=1, foo=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in inner
TypeError: You must specify exactly one of foo, bar
>>> foobar()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in inner
TypeError: You must specify exactly one of foo, bar

The decorator ignores positionals and keyword arguments not included in the list given:

>>> @mutually_exclusive('foo', 'bar')
... def foobar(a,b,c, *, foo=None, bar=None, taz=None):
...     print(a,b,c,foo,bar,taz)
... 
>>> foobar(1,2,3, foo=4, taz=5)
1 2 3 4 None 5
>>> foobar(1,2,3, foo=4, bar=5,taz=6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in inner
TypeError: You must specify exactly one of foo, bar

If the arguments might be "optional" (i.e. you may specify at most one of those keyword arguments, but you may also omit all of them) just change != 1 to <= 1 or in (0,1) as you prefer.

If you replace 1 with a number k you generalize the decorator to accept exactly (or at most) k of the specified arguments from the set you provided.

This however will not help PyCharm in anyway. As far as I know currently it's simply impossible to tell an IDE what you want.


The above decorator has a little "bug": it considers foo=None as if you passed a value for foo since it appears in the kwargs list. Usually you'd expect that passing the default value should behave identically as if you did not specify the argument at all.

Fixing this properly would require to inspect func inside wrapper to lookup the defaults and change k in keywords with something like k in keywords and kwargs[k] != defaults[k].

like image 184
Bakuriu Avatar answered Nov 03 '25 02:11

Bakuriu


The standard library uses a simple runtime check for this:

def foobar(*, foo=None, bar=None):
    if (foo is None) == (bar is None):
        raise ValueError('Exactly one of `foo` and `bar` must be provided')
like image 41
Draconis Avatar answered Nov 03 '25 02:11

Draconis