Count how many arguments passed as positional

Question:

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

Asked By: ignoring_gravity

||

Answers:

You can trick the user and add another argument to the function like this:

def foo(x,y1=None,y=None):
  if y1 is not None:
    print('y was passed positionally!')
  else:
    print('y was passed with its keyword')

I don’t recommend doing it but it does work

Answered By: egjlmn1

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
from inspect import signature

def checkargs(func):
    @wraps(func)
    def inner(*args, **kwargs):
        for param in 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____________

Update Python 3.10

In Python 3.10+ new ParamSpec type annotation was introduced (PEP 612), for better specifying parameter types in higher-order functions. As of now, the correct way to annotate this decorator would be like this:

from functools import wraps
from inspect import signature
from typing import Callable, ParamSpec, TypeVar, TYPE_CHECKING

T = TypeVar("T")
P = ParamSpec("P")


def check_args(func: Callable[P, T]) -> Callable[P, T]:
    """
    Decorator to monitor whether an argument is passed
    positionally or with its keyword, during function call.
    """

    @wraps(func)
    def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        for param in signature(func).parameters:
            if param in kwargs:
                print(param, 'passed with its keyword!')
            else:
                print(param, 'passed positionally.')
        return func(*args, **kwargs)

    return inner

Which correctly preserves type annotation:

if TYPE_CHECKING:
    reveal_type(foo(2, 3))

# ─❯ mypy check_kwd.py
# check_kwd.py:34: note: Revealed type is "builtins.int"
# Success: no issues found in 1 source file
Answered By: Sayandip Dutta

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

See PEP 570 and PEP 3102 for more.

Answered By: Asocia

It is not ordinarily possible. In a sense: the language is not designed to allow you to distinguish both ways.

You can design your function to take different parameters – positional, and named, and check which one was passed, in a thing like:

def foo(x, y=None, /, **kwargs):
 
    if y is None: 
        y = kwargs.pop(y)
        received_as_positional = False
    else:
        received_as_positional = True

The problem is that, although by using positional only parameters as abov, you could get y both ways,
it would be shown not once for a user (or IDE) inspecting the
function signature.

I hav a feeling you just want to know this for the sake of knowing – if
you really intend this for design of an API, I’d suggest you’d rethink
your API – there should be no difference in the behavior, unless both
are un-ambiguously different parameters from the user point of view.

That said, the way to go would be to inspect the caller frame, and check
the bytecode around the place the function is called:


In [24]: import sys, dis

In [25]: def foo(x, y=None):
    ...:     f = sys._getframe().f_back
    ...:     print(dis.dis(f.f_code))
    ...: 

In [26]: foo(1, 2)
  1           0 LOAD_NAME                0 (foo)
              2 LOAD_CONST               0 (1)
              4 LOAD_CONST               1 (2)
              6 CALL_FUNCTION            2
              8 PRINT_EXPR
             10 LOAD_CONST               2 (None)
             12 RETURN_VALUE
None

In [27]: foo(1, y=2)
  1           0 LOAD_NAME                0 (foo)
              2 LOAD_CONST               0 (1)
              4 LOAD_CONST               1 (2)
              6 LOAD_CONST               2 (('y',))
              8 CALL_FUNCTION_KW         2
             10 PRINT_EXPR
             12 LOAD_CONST               3 (None)
             14 RETURN_VALUE

So, as you can see, when y is called as named parameter, the opcode for the call is CALL_FUNCTION_KW , and the name of the parameter is loaded into the stack imediately before it.

Answered By: jsbueno

In foo, you can pass the call stack from traceback to positionally, which will then parse the lines, find the line where foo itself is called, and then parse the line with ast to locate positional parameter specifications (if any):

import traceback, ast, re
def get_fun(name, ast_obj):
    if isinstance(ast_obj, ast.Call) and ast_obj.func.id == name:
        yield from [i.arg for i in getattr(ast_obj, 'keywords', [])]
    for a, b in getattr(ast_obj, '__dict__', {}).items():
        yield from (get_fun(name, b) if not isinstance(b, list) else 
                        [i for k in b for i in get_fun(name, k)])

def passed_positionally(stack):
    *_, [_, co], [trace, _] = [re.split('ns+', i.strip()) for i in stack] 
    f_name = re.findall('(?:line d+, in )(w+)', trace)[0]
    return list(get_fun(f_name, ast.parse(co)))

def foo(x, y):
    if 'y' in passed_positionally(traceback.format_stack()):
        print('y was passed with its keyword')
    else:
        print('y was passed positionally')

foo(1, y=2)

Output:

y was passed with its keyword

Notes:

  1. This solution does not require any wrapping of foo. Only the traceback needs to be captured.
  2. To get the full foo call as a string in the traceback, this solution must be run in a file, not the shell.
Answered By: Ajax1234

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!
Answered By: ignoring_gravity