Useful Python 3 features (advanced)¶
Keyword-only Arguments¶
Note
Keyword-only arguments are valid in Python 3. Code that uses them will not run under Python 2.
Interfaces, especially useful ones, are prone to being changed over time. This can present a problem, since when one changes an interface in an incompatible way, all users of that interface must also be updated. Since the author of an interface and the person using that interface are not always the same person, this issue can lead to a considerable amount of churn, pain, breakage, and gnashing of teeth.
To explain this, we need to review the different kinds of parameters that a python function can be defined to take. The simplest is a positional parameter. The following function has two positional parameters
def positional_function(foo, bar):
pass
Positional parameters are the simplest, and default kind of parameter. In addition, both Python 2 and 3 also allow keyword arguments. These kinds of arguments make it possible to define a default value for a parameter
def keyword_function(foo=3, bar=4)
One can specify the values of positional arguments when calling functions in two ways. The following are all valid
positional_function(3, 4)
positional_function(3, bar=4)
positional_function(foo=3, bar=4)
However, the following is not valid
positional_function(4, foo=3)
This will raise a TypeError
, because Python interprets this line to mean
that foo is getting specified twice. For positional arguments, the order of
the arguments matters.
Just like positional arguments, keyword arguments can be specified using their order, or by specifying their names. All of these are valid
keyword_function(3, 4)
keyword_function(3, bar=4)
keyword_function(foo=3, bar=4)
Now that we know the difference between positional arguments and keyword arguments, we can introduce keyword-only arguments. These are only specifiable via the name of the argument, and cannot be specified as a positional argument.
For example, the following function takes a positional argument and two keyword-only arguments
def keyword_only_function(parameter, *, option1=False, option2=''):
pass
In this example, option1
, and option2
are only specifiable via the
keyword argument syntax. The following is valid
keyword_only_function(3, option1=True, option2='Hello World!')
But this example will raise an error
keyword_only_function(3, True, 'Hello World!')
The option to specify that a parameter is keyword-only makes it possible ensure that users of a function cannot accidentally use an option that is controlled by a keyword-only argument. It also becomes possible to reorder keyword-only arguments in a function signature, since keyword-only arguments are only accessible via the name of the argument, not its position in the function signature.
Chained Exceptions¶
In python, the most natural way to communicate about an error is to raise an exception. If the error state is recoverable, the exception might be caught elsewhere and then dealt with. If it is not recoverable, the original exception might be re-raised to be dealt with at a higher level, or a completely new exception could be raised.
In Python 2, the error message that gets printed out when an exception is raised to the user level only contains information about the last exception that was raised. Information from intermediate exceptions generated at lower levels in the code are lost unless care is taken to re-raise the exception with the appropriate information and context. This can be painful, especially when debugging a library, as the error message containing information about the real error will get discarded in favor of a more generic error message.
Take the following short example
my_dict = {'a': 1, 'b': 2}
try:
value = my_dict['c']
except KeyError:
raise RuntimeError("dict access failed")
In Python 3, executing this snippet will print the following
Traceback (most recent call last):
File "test.py", line 4, in <module>
value = my_dict['c']
KeyError: 'c'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "test.py", line 6, in <module>
raise RuntimeError("dict access failed")
RuntimeError: dict access failed
Under Python 2, you will see a much less useful error message
Traceback (most recent call last):
File "test.py", line 6, in <module>
raise RuntimeError("dict access failed")
RuntimeError: dict access failed
Even in this contrived example you can see how the extra information from the original exception can ease debugging. Note how under Python 3, the original exception is printed out, along with the original traceback. This makes it possible to immediately see where the original exception was raised, and where error handling code is re-raising another exception. In real code, where errors might propagate between files and in the worst case, across complex codebases, this extra information can be enough to head off an afternoon of fruitless head scratching and troubleshooting.