If I have a function
def foo(x, y):
pass
how can I tell, from inside the function, whether y was passed positionally or with its keyword?
I'd like to have something like
def foo(x, y):
if passed_positionally(y):
print('y was passed positionally!')
else:
print('y was passed with its keyword')
so that I get
>>> foo(3, 4)
y was passed positionally
>>> foo(3, y=4)
y was passed with its keyword
I realise I didn't originally specify this, but is it possible to do this whilst preserving type annotations? The top answer so far suggests using a decorator - however, that does not preserve the return type
You can create a decorator, like this:
def checkargs(func):
def inner(*args, **kwargs):
if 'y' in kwargs:
print('y passed with its keyword!')
else:
print('y passed positionally.')
result = func(*args, **kwargs)
return result
return inner
>>> @checkargs
...: def foo(x, y):
...: return x + y
>>> foo(2, 3)
y passed positionally.
5
>>> foo(2, y=3)
y passed with its keyword!
5
Of course you can improve this by allowing the decorator to accept arguments. Thus you can pass the parameter you want to check for. Which would be something like this:
def checkargs(param_to_check):
def inner(func):
def wrapper(*args, **kwargs):
if param_to_check in kwargs:
print('y passed with its keyword!')
else:
print('y passed positionally.')
result = func(*args, **kwargs)
return result
return wrapper
return inner
>>> @checkargs(param_to_check='y')
...: def foo(x, y):
...: return x + y
>>> foo(2, y=3)
y passed with its keyword!
5
I think adding functools.wraps would preserve the annotations, following version also allows to perform the check over all arguments (using inspect):
from functools import wraps
import inspect
def checkargs(func):
@wraps(func)
def inner(*args, **kwargs):
for param in inspect.signature(func).parameters:
if param in kwargs:
print(param, 'passed with its keyword!')
else:
print(param, 'passed positionally.')
result = func(*args, **kwargs)
return result
return inner
>>> @checkargs
...: def foo(x, y, z) -> int:
...: return x + y
>>> foo(2, 3, z=4)
x passed positionally.
y passed positionally.
z passed with its keyword!
9
>>> inspect.getfullargspec(foo)
FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None,
kwonlyargs=[], kwonlydefaults=None, annotations={'return': <class 'int'>})
_____________HERE____________
At the end, if you are going to do something like this:
def foo(x, y):
if passed_positionally(y):
raise Exception("You need to pass 'y' as a keyword argument")
else:
process(x, y)
You can do this:
def foo(x, *, y):
pass
>>> foo(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() takes 1 positional argument but 2 were given
>>> foo(1, y=2) # works
Or only allow them to be passed positionally:
def foo(x, y, /):
pass
>>> foo(x=1, y=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got some positional-only arguments passed as keyword arguments: 'x, y'
>>> foo(1, 2) # works
Adapted from @Cyttorak 's answer, here's a way to do it which maintains the types:
from typing import TypeVar, Callable, Any, TYPE_CHECKING
T = TypeVar("T", bound=Callable[..., Any])
from functools import wraps
import inspect
def checkargs() -> Callable[[T], T]:
def decorate(func):
@wraps(func)
def inner(*args, **kwargs):
for param in inspect.signature(func).parameters:
if param in kwargs:
print(param, 'passed with its keyword!')
else:
print(param, 'passed positionally.')
result = func(*args, **kwargs)
return result
return inner
return decorate
@checkargs()
def foo(x, y) -> int:
return x+y
if TYPE_CHECKING:
reveal_type(foo(2, 3))
foo(2, 3)
foo(2, y=3)
Output is:
$ mypy t.py
t.py:27: note: Revealed type is 'builtins.int'
$ python t.py
x passed positionally.
y passed positionally.
x passed positionally.
y passed with its keyword!