Summary
Yes, this is possible.
In the example in the question, the anotherfunc
parameter for myfunc
is an example of a callback, and myfunc
is therefore an example of a higher-order function (hereafter, HOF).
A simple example of both sides of the equation - writing the HOF and giving it a callback - might look like:
def higher_order(a_callback):
print("I will call:", a_callback)
a_callback()
def my_callback():
print("my_callback was called")
higher_order(my_callback)
Note carefully that the example passes my_callback
- using just the function name, not with parentheses after it. Incorrectly writing higher_order(my_callback())
would mean to call my_callback
first, and pass the return value (here, that would be None
) to higher_order
. This will cause a TypeError
, since None
is not callable.
In the function itself, nothing special needs to be done in order to accept another function as a parameter, nor to use it by calling it. Inside higher_order
, a_callback
is the local name for whatever function was passed in (here, my_callback
); functions are called by writing their name, (
, the appropriate arguments, and )
; so that is all that higher_order
needs to do in order to use that passed-in function.
Writing a HOF
Suppose we attempt to define def my_func(other_func, func_args):
, where other_func
will be a callback. Within the function, other_func
is simply a name for the callback that was passed in, and calling it works the same way as calling any other function. We need a name (or any other expression that evaluates to the callable that should be called), then (
, then any appropriate arguments for the call, then )
. Supposing for example that func_args
should be a sequence of variable arguments to the callable, we can make this call by unpacking the arguments in the call. Thus:
def my_func(other_func, func_args):
other_func(*func_args)
Similarly, a callback that requires keyword arguments could receive them from another parameter that will be passed a dict
(or other mapping), which the HOF could pass to the callback with **
unpacking. Thus:
def my_func(other_func, func_args, func_kwargs):
other_func(*func_args, **func_kwargs)
Of course, we are by no means limited to such basic logic. my_func
works like any other function. It could do any other arbitrary work before or after calling other_func
; it could return
or otherwise make use of the other_func
result; it could call other_func
more than once (or conditionally not at all); it could use its own logic to determine arguments to pass to the callback (perhaps even determine them locally and not have parameters like func_args
or func_kwargs
at all), etc.
Passing a callback function to a HOF
In order to use this HOF, the calling code requires two things: an appropriate callable to pass as the callback (i.e., its signature must be compatible with how the HOF will call it), and the appropriate code for a call to the HOF itself.
Continuing the above example, suppose we have a callback like
def my_callback(a, b, /, **c):
print(f'a = {a}; b = {b}; c = {c}')
Since the previous my_func
will use *
and **
for the call, applied to input from the caller, there is no particular restriction on the signature of my_callback
. However, since my_func
will receive the a
and b
arguments from *func_args
, and since my_func
marks these parameters as positional-only, the func_args
passed to my_func
will need to be a sequence of length 2. (func_kwargs
is supposed to be a dictionary anyway; it will be unpacked for the call to the callback, and then the callback will pack it again.
Thus:
def my_func(other_func, func_args, func_kwargs):
other_func(*func_args, **func_kwargs)
def my_callback(a, b, /, **c):
print(f'a = {a}; b = {b}; c = {c}')
# call `my_func`, giving it the callback `my_callback`
# as well as appropriate arguments to call the callback:
my_func(my_callback, [1, 2], {'example': 3})
Other kinds of callbacks
Since the HOF simply calls the callback, it doesn't actually care whether the callback is a function. Taking advantage of duck typing, we can also pass e.g. a class. This is especially useful for HOFs that use a callback for "type-checking" (e.g. the standard library argparse
does this):
def ask_user_for_value(type_checker):
while True:
try:
return type_checker(input('give me some input: '))
except Exception as e:
print(f'invalid input ({e}); try again')
# using an existing type:
ask_user_for_value(int)
# using a custom function for verification:
def capital_letter(text):
if len(text) != 1:
raise ValueError('not a single character')
if not (text.isupper() and text.isalpha()):
raise ValueError('not an uppercase letter')
return text
ask_user_for_value(capital_letter)
# using an enum: (in 3.11 this could use StrEnum)
from enum import Enum
class Example(Enum):
ONE = 'one'
TWO = 'two'
ask_user_for_value(Example)
# using a bound method of an instance:
class Exact: # only allow the specified string
def __init__(self, value):
self._value = value
def check(self, value):
if value != self._value:
raise ValueError(f'must be {self._value!r}')
return value
ask_user_for_value(Exact('password').check)
Other ways to use callbacks
Aside from defining a HOF, a callback function can simply be stored in a data structure - such as a list
, dict
or as an attribute of some class instance - and then used later. The key insight is that functions are objects in Python, so they can be stored in this way, and any expression that evaluates to the function object can be used to call the function.
Examples:
def my_callback():
print('my_callback was called')
# in a list
funcs = [my_callback]
for i in funcs:
i()
# in a dict
funcs = {'my_callback': my_callback}
funcs['my_callback']()
# in a class instance
class Example:
def __init__(self, callback):
self._callback = callback
def use_callback(self):
self._callback()
instance = Example()
instance.use_callback()
Special case: providing arguments that the HOF won't
Sometimes, we want to use an existing callback function, but it requires additional arguments besides what the HOF will provide. This is especially important when working with a HOF that comes from third-party code. Many libraries are specifically designed to accept arbitrary parameters to forward to the callback (for example, the standard library threading
), but others are not (for example, using the standard library timeit
module with a callable rather than a string for the code to test).
In the latter case, arguments must be "bound" to the callback before passing it to the HOF.
See Python Argument Binders to understand how to do this - it is beyond the scope of this answer.
The same logic, of course, applies when a callback is stored for later use in other ways (e.g. as a command
provided when creating a Button
in Tkinter).