Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python

"Sometimes the keys and values don't always get passed up to where they need to be, because the MRO falls down..."

"... and python fails to put things where they need to go".

When Kenneth says that, he doesn't explain why this happens. It seems VERY random to me because when he does this:

kenneth = Thief("Kenneth", sneaky = False)

In my eyes the code should run perfectly fine and not raise a TypeError: missing 1 required positional argument 'name'.

Since "Kenneth" goes to *args, when it gets passed to Character's init, it goes unpacked and as the first argument. So I thought.

But Kenneth says that by the time it goes to the super calls, it's lost this positional argument and it doesn't put this "arg" as the first argument that is provided.

Look, when I do this:

class Character:
    def __init__(self, name, **kwargs):
        self.name = name

        for key, value in kwargs.items():
            setattr(self, key, value)


class Sneaky:
    sneaky = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

When I create an instance:

ewerton = Thief("Ewerton", age = 28)

# it returns ewerton.name = 'Ewerton' and ewerton.age = 28

args was not lost in this case and it was unpacked in the Character's class name variable.

When can I know for sure *args will be delivered correctly in the positional argument?

1 Answer

Chris Freeman
MOD
Chris Freeman
Treehouse Moderator 68,468 Points

Great question Ewerton Luna!

TL;DR: You can not depend on *args to pass along positional arguments due to the unknown number of arguments consumed by a variable number of parent classes ahead of a another class in the Method Resolution Order.

Some baseline terms.

  • Arguments are used to call methods and they come in two forms: with and without keywords. Arguments without keywords are also know as positional arguments. All arguments with keywords must be listed following any arguments without keywords.
  • Parameters are defined in methods and they receive assignments from the calling arguments. Parameters also come in two forms: with and without default values.

An important distinction is that there is no relationship between concepts of parameters with defaults and arguments with keywords.

Also keep in mind that

  • *args is used to contain an arbitrary number of positional arguments. In a method definition, any extra (leftover) received positional arguments are packed into *args.
  • **kwargs is used to contain an arbitrary number of keyword arguments. In a method definition, any extra (leftover) received keyword arguments are packed into **kwargs.

* star and ** star conventions represent packing and unpacking of arguments. The *args used in a calling statement does not necessarily align with the *argsseen within a parameter list. The packing ofargsandkwargs` occurs before the call is made. The unpacking occurs before they are mapped to the method's parameters and any leftover are packed into the **new* *args and **kwargs, if present, in the parameter list.

In a pathological case, a method could have a parameter list such as pathological_method(first_arg, *middle_args, last_arg). In this case, the calling args (1, 2, 3, 4, 5) could be represented by *args as in method_call(*args), the receiving method would get first_arg as 1, last_arg as 5, and *middle_args as (2, 3, 4).

Python assigns arguments in an aggressive manner. Consider the two scenarios:

  • If there is a parameter with a default value that has not been assigned using a keyword argument, it will get assigned the next positional argument in the list.
  • If there is a parameter without a default value, it will also get assigned next positional argument in the list, even if there is also a keyword assignment to the same parameter name.

To test out these ideas, consider the RPG example from the video, ZIP files available here. Adding the following debug print statements as the first line in each corresponding __init__ methods:

        print(f"running Agile.__init__ args:{args}, kwargs:{kwargs}")
        print(f"running Sneaky.__init__ args:{args}, kwargs:{kwargs}")
        print(f"running Character.__init__ kwargs:{kwargs}")

Note that Character does not contain a *args catchall. This means any leftover positional arguments beyond the first, has no where to go. The first, if present will get assigned to name regardless if there is also a keyword argument name=value.

Instantiating Thief with positional arguments only shows quickly the issue:

$ python
Python 3.6.3 (default, Oct  3 2017, 21:45:48) 
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from thieves import Thief
>>> t1 = Thief('bob')
running Agile.__init__ args:(), kwargs:{}
running Sneaky.__init__ args:(), kwargs:{}
running Character.__init__ kwargs:{}
>>> t1.__dict__
{'name': '', 'sneaky': True, 'agile': 'bob'}

Since agile was not assigned with a keyword argument, it was assigned the first available positional argument. The attribute sneaky received its default value, and name got its default value of the empty string. Since Agile consumed the only argument, there were were no *args or **kwargs left for the debug print statements.

Since, there isn't value checking on agile or sneaky, I'll use numbers so they're easily tracked. Using two positional arguments gets a bit better:

>>> t1 = Thief(15, 'bob')
running Agile.__init__ args:('bob',), kwargs:{}
running Sneaky.__init__ args:(), kwargs:{}
running Character.__init__ kwargs:{}
>>> t1.__dict__
{'name': '', 'sneaky': 'bob', 'agile': 15}

Now agile gets the first positional argument 15, but now Sneaky grabs the name as its positional argument for sneaky. Notice how the name "bob" shows up in the *args since it is leftover after the assignment to agile.

Let's go to three arguments:

>>> t1 = Thief(15, 42, 'bob')
running Agile.__init__ args:(42, 'bob'), kwargs:{}
running Sneaky.__init__ args:('bob',), kwargs:{}
running Character.__init__ kwargs:{}
>>> t1.__dict__
{'name': 'bob', 'sneaky': 42, 'agile': 15}

Here, Agile passes 42, 'bob' in *args and Sneaky passes 'bob' in *args. Now all values reach where they are intended to be. The downside is that if the number of attribute classes changes, the number of required positional arguments to get the name to Character will also change. This causes very fragile code.

Python, of course, catches if there are too many positional arguments since Character does not have a catch-all *args for the overflow:

>>> t1 = Thief(15, 42, 'bob', 3.14159)
running Agile.__init__ args:(42, 'bob', 3.14159), kwargs:{}
running Sneaky.__init__ args:('bob', 3.14159), kwargs:{}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/chris/devel/Treehouse/forum/rpg/attributes.py", line 22, in __init__
    super().__init__(*args, **kwargs)
  File "/home/chris/devel/Treehouse/forum/rpg/attributes.py", line 9, in __init__
    super().__init__(*args, **kwargs)
TypeError: __init__() takes from 1 to 2 positional arguments but 3 were given

Since there is no *args in Character.__init__(), the super() call in Sneaky blows up with too many positional arguments.

A final consideration is depending on the "correct" number of positional arguments makes the code very fragile in cases where you would like to add or subtract other attribute classes, say a Magical trait. Any code depending on the "correct" number of positional arguments would break, or worse, have a silent bug where positional argument values are being assigned to the wrong parameters.

In the end, using only positional arguments may work, if the number of positional arguments exactly matches the number of arguments consumed by all parent class __init__ methods. In this case, exactly three, with "Kenneth" being the last one.

Post back if you have any more questions. Good luck!!

Woooooow!!! What an amazing explanation, best ever! I loved it! Thank you so much, man! You just made so many things more clear to me now. Thank you!!!