Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get __init__() to raise a more useful exception instead of TypeError when incorrect # of arguments?

I have a class with an init that takes many arguments--some required, some not.

If one of the needed arguments is not supplied, you may get a TypeError, with an unhelpful message like 'requires at least 10 arguments (14 given)'.

I would like to have a custom TypeError subclass that actually informs the user of which arguments are missing.

like image 283
Kyle Baker Avatar asked Jun 23 '16 01:06

Kyle Baker


3 Answers

If you want to use kwargs, you can set in init a list of required arguments and then check if all required kwargs are present. Check out the following example.

class Something():
    def __init__(self, **kwargs):
        required_args = ['arg_1', 'arg_2']

        for arg in required_args:
            if arg not in kwargs:
                raise TypeError("Argument %s is required" % arg)

obj = Something(arg_1=77, arg_3=2)
like image 146
Tomás Gonzalez Dowling Avatar answered Nov 12 '22 03:11

Tomás Gonzalez Dowling


In Python 3, you can define a function that takes "required keyword-only arguments." This is clearest documented in PEP 3102. The error message you get when you omit required keyword-only arguments includes the argument names.

$ python3
Python 3.5.2rc1 (default, Jun 13 2016, 09:33:26) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.

>>> class X:
...   def __init__(self, *, foo, bar, baz):
...     self.foo = foo
...     self.bar = bar
...     self.baz = baz
... 
>>> a = X(foo=1,bar=2,baz=3)
# no error
>>> b = X(foo=1,bar=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required keyword-only argument: 'baz'
>>> b = X(foo=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required keyword-only arguments: 'bar' and 'baz'

However, this is not compatible with code that expects to be able to call X() with positional arguments, and the error message you get is still the one you don't like:

>>> a = X(1,2,3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes 1 positional argument but 4 were given

Also, this feature is not available in any version of Python 2:

$ python
Python 2.7.12rc1 (default, Jun 13 2016, 09:20:59) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> class X(object):
...    def __init__(self, *, foo, bar, baz):
  File "<stdin>", line 2
    def __init__(self, *, foo, bar, baz):
                        ^
SyntaxError: invalid syntax

Improving the diagnostics given for positional arguments would probably involve hacking the interpreter. The Python development team might be amenable to patches; I'd consider bringing this up on the python-ideas mailing list.

like image 22
zwol Avatar answered Nov 12 '22 01:11

zwol


I ended up using a variation off of Tomas Gonzalez's answer, rewriting it so that it supports the mixture of ordered args, keyword args, and default values that existed. This supported our requirement of writing code that is always backwards compatible with legacy code. For 'greenfield' implementations, Tomas' answer is preferable (and one would just always make calls with kwargs). For legacy situations like mine (or ones wherein a combination of ordered required and non-required arguments may be necessary?), consider the following:

def __init__(*args, **kwargs):
    required_args = ['a', 'b']

    # to support legacy use that may included ordered args from previous iteration of this function
    ordered_args = ['a', 'b', 'c', 'd', 'e']

    default_args = {
        'c': 0,
        'd': 1,
        'e': 2
    }

    for index, value in enumerate(args):
        kwargs[ordered_args[index]] = value

    for arg in required_args:
        if arg not in kwargs:
            raise TypeError("Argument %s is required" % arg)

    for arg in default_args:
        if arg not in kwargs:
            kwargs[arg] = default_args[arg]

While this is an acceptable hack, I was really hoping to subclass the native TypeError raised, so that the original function would still look 'normal'.

like image 1
Kyle Baker Avatar answered Nov 12 '22 01:11

Kyle Baker