199

In Python you may have a function definition:

def info(obj, spacing=10, collapse=1)

which could be called in any of the following ways:

info(odbchelper)                    
info(odbchelper, 12)                
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

thanks to Python's allowing of any-order arguments, so long as they're named.

The problem we're having is as some of our larger functions grow, people might be adding parameters between spacing and collapse, meaning that the wrong values may be going to parameters that aren't named. In addition sometimes it's not always clear as to what needs to go in.

How can we force people to name certain parameters - not just a coding standard, but ideally a flag or pydev plugin?

So that in the above 4 examples, only the last would pass the check as all the parameters are named.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
Mark Mayo
  • 12,230
  • 12
  • 54
  • 85

11 Answers11

361

In Python 3 - Yes, you can specify * in the argument list.

From docs:

Parameters after “*” or “*identifier” are keyword-only parameters and may only be passed used keyword arguments.

>>> def foo(pos, *, forcenamed):
...   print(pos, forcenamed)
... 
>>> foo(pos=10, forcenamed=20)
10 20
>>> foo(10, forcenamed=20)
10 20
>>> foo(10, 20)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes exactly 1 positional argument (2 given)

This can also be combined with **kwargs:

def foo(pos, *, forcenamed, **kwargs):

To complete example:

def foo(pos, *, forcenamed ):
    print(pos, forcenamed)

foo(pos=10, forcenamed=20)
foo(10, forcenamed=20)
# basically you always have to give the value!
foo(10)

output:

Traceback (most recent call last):
  File "/Users/brando/anaconda3/envs/metalearning/lib/python3.9/site-packages/IPython/core/interactiveshell.py", line 3444, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-12-ab74191b3e9e>", line 7, in <module>
    foo(10)
TypeError: foo() missing 1 required keyword-only argument: 'forcenamed'

So you are forced to always give the value. If you don't call it you don't have to do anything else named argument forced.

Charlie Parker
  • 5,884
  • 57
  • 198
  • 323
Eli Bendersky
  • 263,248
  • 89
  • 350
  • 412
  • In addition to the response. You can consider also using Python typing hints feature to enforce your function signature. This way you can detect bad calls by using mypy checks for example. – rkachach Apr 05 '22 at 08:59
  • PEP 3102 – Keyword-Only Arguments: https://peps.python.org/pep-3102/ – Jean Monet May 27 '22 at 07:51
  • Note: If the function has a decorator, this will not work. Instead, forward to a second (private) function with the decorator. – kevinarpe Jun 02 '23 at 06:48
63

You can force people to use keyword arguments in Python3 by defining a function in the following way.

def foo(*, arg0="default0", arg1="default1", arg2="default2"):
    pass

By making the first argument a positional argument with no name you force everyone who calls the function to use the keyword arguments which is what I think you were asking about. In Python2 the only way to do this is to define a function like this

def foo(**kwargs):
    pass

That'll force the caller to use kwargs but this isn't that great of a solution as you'd then have to put a check to only accept the argument that you need.

martega
  • 2,103
  • 2
  • 21
  • 33
14

True, most programming languages make parameter order part of the function call contract, but this doesn't need to be so. Why would it? My understanding of the question is, then, if Python is any different to other programming languages in this respect. In addition to other good answers for Python 2, please consider the following:

__named_only_start = object()

def info(param1,param2,param3,_p=__named_only_start,spacing=10,collapse=1):
    if _p is not __named_only_start:
        raise TypeError("info() takes at most 3 positional arguments")
    return str(param1+param2+param3) +"-"+ str(spacing) +"-"+ str(collapse)

The only way a caller would be able to provide arguments spacing and collapse positionally (without an exception) would be:

info(arg1, arg2, arg3, module.__named_only_start, 11, 2)

The convention of not using private elements belonging to other modules already is very basic in Python. As with Python itself, this convention for parameters would only be semi-enforced.

Otherwise, calls would need to be of the form:

info(arg1, arg2, arg3, spacing=11, collapse=2)

A call

info(arg1, arg2, arg3, 11, 2)

would assign value 11 to parameter _p and an exception risen by the function's first instruction.

Characteristics:

  • Parameters before _p=__named_only_start are admitted positionally (or by name).
  • Parameters after _p=__named_only_start must be provided by name only (unless knowledge about the special sentinel object __named_only_start is obtained and used).

Pros:

  • Parameters are explicit in number and meaning (the later if good names are also chosen, of course).
  • If the sentinel is specified as first parameter, then all arguments need to be specified by name.
  • When calling the function, it's possible to switch to positional mode by using the sentinel object __named_only_start in the corresponding position.
  • A better performance than other alternatives can be anticipated.

Cons:

  • Checking occurs during run-time, not compile-time.
  • Use of an extra parameter (though not argument) and an additional check. Small performance degradation respect to regular functions.
  • Functionality is a hack without direct support by the language (see note below).
  • When calling the function, it's possible to switch to positional mode by using the sentinel object __named_only_start in the right position. Yes, this can also be seen as a pro.

Please do keep in mind that this answer is only valid for Python 2. Python 3 implements the similar, but very elegant, language-supported mechanism described in other answers.

I've found that when I open my mind and think about it, no question or other's decision seems stupid, dumb, or just silly. Quite on the contrary: I typically learn a lot.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Mario Rossi
  • 7,651
  • 27
  • 37
  • _"Checking occurs during run-time, not compile-time."_ - I think that's true of all function argument checking. Until you actually execute the line of the function invocation, you don't always know which function is being executed. Also, **+1** - this is clever. – Eric Jul 23 '13 at 18:24
  • @Eric: It's just that I'd have preferred static checking. But you are right: that wouldn't have been Python at all. Though not a decisive point, Python 3's "*" construct is also dynamically checked. Thanks for your comment. – Mario Rossi Jul 27 '13 at 05:42
  • Also, if you name the module variable `_named_only_start`, it becomes impossible to reference it from an external module, which takes out a pro and a con. (single leading underscores at module scope are private, IIRC) – Eric Jul 27 '13 at 23:54
  • Regarding naming of the sentinel, we could also have both a `__named_only_start` and a `named_only_start` (no initial underscore), the second to indicate that named mode is "recommended", but not to the level of being "actively promoted" (as one is public and the other is not). Regarding the "privateness" of `_names` starting with underscores, it's not very strongly enforced by the language: it can be easily circumvented by the use of specific (non-*) imports or qualified names. This is why several Python documents prefer to use the term "non-public" instead of "private". – Mario Rossi Jul 29 '13 at 23:02
10

You can do that in a way that works in both Python 2 and Python 3, by making a "bogus" first keyword argument with a default value that will not occur "naturally". That keyword argument can be preceded by one or more arguments without value:

_dummy = object()

def info(object, _kw=_dummy, spacing=10, collapse=1):
    if _kw is not _dummy:
        raise TypeError("info() takes 1 positional argument but at least 2 were given")

This will allow:

info(odbchelper)        
info(odbchelper, collapse=0)        
info(spacing=15, object=odbchelper)

but not:

info(odbchelper, 12)                

If you change the function to:

def info(_kw=_dummy, spacing=10, collapse=1):

then all arguments must have keywords and info(odbchelper) will no longer work.

This will allow you to position additional keyword arguments any place after _kw, without forcing you to put them after the last entry. This often makes sense, e.g. grouping thing logically or arranging keywords alphabetically can help with maintenance and development.

So there is no need to revert to using def(**kwargs) and losing the signature information in your smart editor. Your social contract is to provide certain information, by forcing (some of them) to require keywords, the order these are presented in, has become irrelevant.

Anthon
  • 69,918
  • 32
  • 186
  • 246
4

The python3 keyword-only arguments (*) can be simulated in python2.x with **kwargs

Consider the following python3 code:

def f(pos_arg, *, no_default, has_default='default'):
    print(pos_arg, no_default, has_default)

and its behaviour:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 3 were given
>>> f(1, no_default='hi')
1 hi default
>>> f(1, no_default='hi', has_default='hello')
1 hi hello
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required keyword-only argument: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() got an unexpected keyword argument 'wat'

This can be simulated using the following, note I've taken the liberty of switching TypeError to KeyError in the "required named argument" case, it wouldn't be too much work to make that the same exception type as well

def f(pos_arg, **kwargs):
    no_default = kwargs.pop('no_default')
    has_default = kwargs.pop('has_default', 'default')
    if kwargs:
        raise TypeError('unexpected keyword argument(s) {}'.format(', '.join(sorted(kwargs))))

    print(pos_arg, no_default, has_default)

And behaviour:

>>> f(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 1 argument (3 given)
>>> f(1, no_default='hi')
(1, 'hi', 'default')
>>> f(1, no_default='hi', has_default='hello')
(1, 'hi', 'hello')
>>> f(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
KeyError: 'no_default'
>>> f(1, no_default=1, wat='wat')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in f
TypeError: unexpected keyword argument(s) wat

The recipe works equally as well in python3.x, but should be avoided if you are python3.x only

anthony sottile
  • 61,815
  • 15
  • 148
  • 207
  • Ah, so `kwargs.pop('foo')` is a Python 2 idiom? I need to update my coding style. I was still using this approach in Python 3 – Neil Jun 18 '19 at 10:04
2

Update:

I realized that using **kwargs would not solve the problem. If your programmers change function arguments as they wish, one could, for example, change the function to this:

def info(foo, **kwargs):

and the old code would break again (because now every function call has to include the first argument).

It really comes down to what Bryan says.


(...) people might be adding parameters between spacing and collapse (...)

In general, when changing functions, new arguments should always go to the end. Otherwise it breaks the code. Should be obvious.
If someone changes the function so that the code breaks, this change has to be rejected.
(As Bryan says, it is like a contract)

(...) sometimes it's not always clear as to what needs to go in.

By looking at the signature of the function (i.e def info(object, spacing=10, collapse=1) ) one should immediately see that every argument that has not a default value, is mandatory.
What the argument is for, should go into the docstring.


Old answer (kept for completeness):

This is probably not a good solution:

You can define functions this way:

def info(**kwargs):
    ''' Some docstring here describing possible and mandatory arguments. '''
    spacing = kwargs.get('spacing', 15)
    obj = kwargs.get('object', None)
    if not obj:
       raise ValueError('object is needed')

kwargs is a dictionary that contains any keyword argument. You can check whether a mandatory argument is present and if not, raise an exception.

The downside is, that it might not be that obvious anymore, which arguments are possible, but with a proper docstring, it should be fine.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • 3
    I liked your old answer better. Just put a comment as to why you are only accepting **kwargs in the function. After all, anyone can change anything in source code -- you need documentation to describe intent and purpose behind your decisions. – Brandon Jun 03 '10 at 11:57
  • There's no actual answer in this answer! – Phil Jul 29 '20 at 12:10
  • As @Phil says, as currently stands i.e. an update with the old answer effectively scratched, there is no actual answer in this post. Yeah, this post is over 10 years old though.. let the relics of time be at work. – Nuclear03020704 Apr 03 '22 at 06:48
1

As other answers say, changing function signatures is a bad idea. Either add new parameters to the end, or fix every caller if arguments are inserted.

If you still want to do it, use a function decorator and the inspect.getargspec function. It would be used something like this:

@require_named_args
def info(object, spacing=10, collapse=1):
    ....

Implementation of require_named_args is left as an exercise for the reader.

I would not bother. It will be slow every time the function is called, and you will get better results from writing code more carefully.

Daniel Newby
  • 2,392
  • 18
  • 15
0

You could declare your functions as receiving **args only. That would mandate keyword arguments but you'd have some extra work to make sure only valid names are passed in.

def foo(**args):
   print args

foo(1,2) # Raises TypeError: foo() takes exactly 0 arguments (2 given)
foo(hello = 1, goodbye = 2) # Works fine.
Noufal Ibrahim
  • 71,383
  • 13
  • 135
  • 169
  • 1
    Not only do you have to add keyword checks, but think about a consumer that knows they have to call a method with the signature `foo(**kwargs)`. What do I pass into that? `foo(killme=True, when="rightnowplease")` – Dagrooms Dec 19 '17 at 22:40
  • It depends really. Consider `dict`. – Noufal Ibrahim Dec 20 '17 at 04:43
-1

You could use the ** operator:

def info(**kwargs):

this way people are forced to use named parameters.

Olivier Verdier
  • 46,998
  • 29
  • 98
  • 90
-1
def cheeseshop(kind, *arguments, **keywords):

in python if use *args that means you can pass n-number of positional arguments for this parameter - which will be accessed as a tuple inside the function.

And if use **kw that means its keyword arguments, that can be access as dict - you can pass n-number of kw args, and if you want to restrict that user must enter the sequence and arguments in order then don't use * and ** - (its pythonic way to provide generic solutions for big architectures...)

if you want to restrict your function with default values then you can check inside it

def info(object, spacing, collapse)
  spacing = 10 if spacing is None else spacing
  collapse = 1 if collapse is None else collapse
shahjapan
  • 13,637
  • 22
  • 74
  • 104
  • what happens if spacing is desired to be 0? (answer, you get 10). This answer is as wrong as all the other **kwargs answers for all the same reasons. – Phil Jul 29 '20 at 12:13
  • @phil yes I understood for that I think we should check whether its None or not - have updated the answer. – shahjapan Sep 14 '20 at 06:38
-2

I don't get why a programmer will add a parameter in between two others in the first place.

If you want the function parameters to be used with names (e.g. info(spacing=15, object=odbchelper) ) then it shouldn't matter what order they are defined in, so you might as well put the new parameters at the end.

If you do want the order to matter then can't expect anything to work if you change it!

Umang
  • 5,196
  • 2
  • 25
  • 24
  • 2
    This doesn't answer the question. Whether or not it's a good idea is irrelevant - someone might do it anyway. – Graeme Perrow May 26 '15 at 18:53
  • 2
    As Graeme mentioned, someone will do it anyway. Also, if you're writing a library to be used by others, forcing(python 3 only) the passing of keyword only arguments allows extra flexibility when you have to refactor your API. – s0undt3ch Dec 04 '16 at 01:18