Search code examples
pythontypeerrorkeyword-argument

__init__() got multiple values for keyword argument 'bar'


What seems like a simple pattern for allowing a variable number of positional arguments along with a defaulted named arg doesn't work. I think I can overcome this by using **kwargs, but is there something more straightforward I am doing wrong?

Why doesn't this work?

class Foo(obect):
    """Baffling"""
    def __init__(self, bar=None, *args):
        pass

foo = Foo(1) #works
foo = Foo(1, bar=2) # explodes

----> 1 foo = Foo(1, bar='baz')
TypeError: __init__() got multiple values for keyword argument 'bar'

How is the above code passing a duplicate bar keyword argument?


Solution

  • Named arguments can be passed both by position and by name:

    >>> def foo(a):
    ...     print(a)
    ...
    >>> foo(1)
    1
    >>> foo(a=2)
    2
    

    If you pass positional and keyword arguments, they get assigned in order. In your case, 1 gets assigned to the first positional argument (bar), then bar=2 assigns the the argument named bar. Thus, both assign to the name bar, creating a conflict.


    You can pass additional arguments after your named one:

    >>> def foo(bar=None, *args):
    ...    print('args=%r, bar=%r' % (args, bar))
    ...
    ... foo(2, 1)  # bar=2, *args=(1,)
    args=(1,), bar=2
    

    In Python3, you can also make your parameter keyword only:

    >>> def foo(*args, bar=None):
    ...    print('args=%r, bar=%r' % (args, bar))
    ...
    ... foo(1, bar=2)
    args=(1,), bar=2
    

    This also works when you do not take any variadic arguments:

    >>> def foo(*, bar=None):
    ...    print('args=%r, bar=%r' % ('undefined', bar))
    ...
    ... foo(bar=2)
    args='undefined', bar=2
    >>> foo(1, bar=2)
    TypeError: foo() takes 0 positional arguments but 1 positional argument (and 1 keyword-only argument) were given
    

    In Python2, only **kwargs is allowed after *args. You can emulate named parameters by popping them from kwargs:

    >>> def foo(*args, **kwargs):
    ...    bar = kwargs.pop("bar")
    ...    print('args=%r, bar=%r' % (args, bar))
    ...
    ... foo(1, bar=2)
    args=(1,), bar=2
    

    If you want named-only parameters without variadic positional and named parameters, you must raise errors yourself:

    >>> def foo(*args, **kwargs):
    ...    bar = kwargs.pop('bar')
    ...    if args or kwargs:
    ...        raise TypeError
    ...    print('args=%r, bar=%r' % ('undefined', bar))
    ...
    ... foo(bar=2)
    args='undefined', bar=2
    >>> foo(1, bar=2)
    TypeError