Check if function is callable with given *args and **kwargs

Hi all, I wonder if there is an easy way check if some function is callable with the given arguments.

For example, I got a function with complicated signature

def heavy_function(a, b, /, x, y, *, z, **something_different):
    # sleep for 2 days
    # format disk C
    # send 1000 usd to a random back account

I would like to know if this function would be callable with some *args and **kwargs. For obvious reasons

try:
    heavy_function(*args, **kwargs)
    callable = True
except TypeError:
    callable = False

is not really suitable for me. And function’s signature may be anything, using inspect.signature is possible, but… is there an easier way?

By 2pi360 via Discussions on Python.org at 06Sep2022 14:09:

Hi all, I wonder if there is an easy way check if some function is
callable with the given arguments.
[… long running heavy_function …]
I would like to know if this function would be callable with some
*args and **kwargs. For obvious reasons

try:
   heavy_function(*args, **kwargs)
   callable = True
except TypeError:
   callable = False

is not really suitable for me. And function’s signature may be
anything, using inspect.signature is possible, but… is there an
easier way?

Well, if you put the work into implementing the inspect based
signature check it might be easy from then onward…

Had you considers type annotations? They’re ignored at runtime, but
present. With those, a type checker could inspect the code. It sounds
like you’ve little control over the function you want to check?

Also, see the typeguard module from PyPI:

I routinely use its @typechecked decorator on certainly complicated
functions to do type checking at runtime. The decorator checks arguments
at runtime when the function is called. You might find that the code
could be adapted to do the check without following up with a call, thus
providing the check you want without running the function, provided the
function way annotated with types.

Cheers,
Cameron Simpson cs@cskk.id.au

The short answer is No.

In the most general case, the only way to know whether a function call will succeed is to call it and see if it actually succeeds.

The slightly longer answer is Not Really, But Maybe Sometimes, Kinda Sort Of.

By using introspection on the function, you may sometimes be able to tell whether the arguments you are intending to call will match the signature of the function, for some definition of “match”.

This is what linters, type-checkers, IDEs and smart editors do.

For instance, a linter should be able to flag function calls with too few or too many arguments. If the function signature is def spam(a, b, c) then a linter will know that the calls spam(1) and spam(1, 2, 3, 4) must be wrong, because they don’t match the number of required arguments.

In practice, they work reasonably well, but they are always subject to two kinds of errors:

  • false negatives: the linter doesn’t pick up an actual error;
  • and false positives: the linter wrongly reports something as an error when it isn’t.

The signature of a function tells us, as a human reader:

  • the number of arguments;
  • whether they are positional-only, keyword-only, or positional-or-keyword;
  • if they have type annotations, the acceptable types;
  • whether they are optional or required;
  • and if they are well-named, the meaning of the argument (the semantics).

An introspection function can see the same things, except for the last.

Obviously missing from the list is the acceptable range of values, although that might be hinted at by the name.

For example, if you know that an argument has to be an int, that doesn’t tell you whether negative values or zero are accepted, or whether the number has to be an odd number, or a prime number, or less than 1000, or an even number greater than 4.

If the function has any restrictions on the values, you can’t get that information from the function signature. Only a human reader can determine that, by reading the documentation, or by knowledge of what the function is supposed to do and how it does it.

If the function accepts *args or **kwargs, then things become even more uncertain. Can the function really accept an unrestricted set of keyword arguments? What is it going to do with them?

And lastly, if your function depends on any external environment, say it tries to open a file, or fetch data from the network, then there is no way of knowing whether those external resources will be available without actually trying to use them.

Now we come to the most critical question. Why do you want to do this?

In general, when programming, the programmer knows what function they are calling, and what arguments have to be provided. Linters and type-checkers etc exist to help the programmer avoid mistakes. They aren’t an alternative to knowing what arguments the function needs.

But your description suggests that you have a bunch of functions, and you don’t know what they do or what arguments they need, and you almost want to guess a set of arguments and then check whether or not the call will succeed.

Or you have a single function, but you don’t know what arguments it requires, so you want to try a bunch of random things until it works.

If you explain what you are actually trying to do, and why, we might be able to recommend something:

  • a static linter or type-checker, to avoid programmer errors;
  • a runtime introspection check, to avoid… well I’m not sure what exactly;
  • or just “don’t worry about it”.
1 Like

Thanks for replies!

Here is my use case: I had implement postponed function calls, and I want better exception handling for it. Here an example code to illustrate my idea:

class Backlog:
    def __init__(self):
        self.backlog = []
    def postponed_call(self, fun, *args, **kwargs):
        self.backlog.append([fun, args, kwargs])
    def do_stuff(self):
        for fun, args, kwargs in self.backlog:
            fun(*args, **kwargs)

def f(a, b, c):
    print(a+b+c)

def g(x, y, z):
    print(x+y+z)


backlog = Backlog()
backlog.postponed_call(f, 1, 2, c=3)
backlog.postponed_call(g, 1, 2, c=3)
backlog.do_stuff()

This code should work as expected, however there is a certain inconvenience in debugging it. If I had misspecified the signature of a function when postponing it (i.e. used wrong *args and **kwargs), I will only know it when I run .do_stuff(), and I will have no idea on which line incorrect definition was backlogged. I would like to have something like

    def postponed_call(self, fun, *args, **kwargs):
         if is_callable_with(fun, *args, **kwargs):
             raise TypeError('incompatible args and kwargs declared')
         self.backlog.append([fun, *args, **kwargs])

So that exception is raised immediately on the problematic line. Sure, postponed function f() and g() can also fail inside, and there could be a million other problems, but incorrect argument specification is what bothers me the most.

So currently it seems I need to write this is_callable_with() myself.

PS: while writing it, I thought of an alternative solution. When postponed_call() is called, somehow save the line which called it, and add it to exception if function fails.

Hi 2pi360,

This may help you:

def is_callable_with(f, *args, **kwargs):
    try:
        return inspect.getcallargs(f, *args, **kwargs)
    except TypeError:
        return False
>>> def f(x, /, y, z=3, *args, key=None, value:int=0, **kwargs):
...     pass
...     
>>> is_callable_with(f,0)
False
>>> is_callable_with(f,0,1)
{'x': 0, 'y': 1, 'args': (), 'kwargs': {}, 'z': 3, 'key': None, 'value': 0}
>>> is_callable_with(f,0,1,x=2)
False

https://docs.python.org/3/library/inspect.html#inspect.getcallargs

According to the docs, inspect.getcallargs has been deprecated and Signature.bind is recommended. But, unfortunately the third case shown above fails (maybe a bug). :worried:

>>> inspect.signature(f).bind(0,1,x=2)
<BoundArguments (x=0, y=1, kwargs={'x': 2})>

EDIT Note that inspect.getcallargs doesn’t work properly for decorated functions.

EDIT 2 I reported this to issue-tracker, but one of core developers told me that the return value of inspect.signature(f).bind(0,1,x=2) is correct. So, the return value of inspect.getcallargs is wrong. :worried: → :slightly_smiling_face: Actually, calling f(0,1,x=2) has no problem.

1 Like

That seems like an excellent approach. Regardless of what goes wrong, keeping track of the full call stack will be of value. The traceback module will be helpful here.

Thank you, this is exactly what I was searching! For some reason I have not found getcallargs() myself what looking through documentation. However, it written that getcallargs() is depricated, this model code should use .bind():

def is_callable_with(f, *args, **kwargs):
    try:
        inspect.signature(f).bind(*args, **kwargs)
        return True
    except TypeError:
        return False
1 Like