11. Currying in Python
By Bernd Klein. Last modified: 08 Mar 2024.
General Idea
In mathematics and computer science, currying is the technique of breaking down the evaluation of a function that takes multiple arguments into evaluating a sequence of single-argument functions. Currying is also considered to be a design pattern. Currying is also used in theoretical computer science, because it is often easier to transform multiple argument models into single argument models.
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Origin of the Name
The "Curry" in "Currying" has nothing to do with the spicy curry powder, even though it can make your Python code more spicy. The name is a reference to the logician and mathematician Haskell Brooks Curry, and he is quoted in "THE KLEENE SYMPOSIUM", (Proceedings of the Symposium held June 18-24, 1978 at Madison, Wisconsin. U.S.A.):
Some contemporary logicians call this way of looking at a function “currying”, because I made extensive use of it; but Schonfinkel had the idea some 6 years before I did.
This is why there is also - though seldomly - the name "Schönfinkelisation" being used. There are even further roots of the concept going back to the end of the 19th century and the mathematician Gottlob Frege.
Currying Functions
Currying is the technique of breaking down the evaluation of a function that takes multiple arguments into evaluating a sequence of single-argument functions.
In mathematical notation it looks like this: If we have a function $f$ which takes $n$ arguments, we can 'replace' it by a composition of $n$ functions $f_1, f_2, \ldots f_n$ where each takes only one argument:
we will get the same value x as if we call:
and
An example implemented in Python could look like this:
def f(a1, a2, a3):
return a1 * a2 * a3
def f1(a1):
def f2(a2):
def f3(a3):
return f(a1, a2, a3)
return f3
return f2
for i in range(1, 10):
print(f(i, i+1, i+2), f1(i)(i+1)(i+2))
OUTPUT:
6 6 24 24 60 60 120 120 210 210 336 336 504 504 720 720 990 990
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses
Example BMI
We illustrate this with the function BMI, which we had used in our chapter on functions. We write a curried version of BMI:
def BMI(weight, height):
return weight / height**2
def bmi_curried(height):
def bmi_weight(weight):
return BMI(weight, height)
return bmi_weight
bmi_curried(1.76)(72)
OUTPUT:
23.243801652892564
Using partial
The function partial
of the module functools
of Python allows you to create partially applied functions. A partially applied function is a new function that is derived from an existing function by fixing a certain number of arguments in advance. The result is a function that can be called with the remaining arguments. We can use it for the commposition of functions.
We demonstrate the way of working in the following examples:
from functools import partial
def f(a1, a2):
return a1 * a2
partial(f, 3)(4)
OUTPUT:
12
from functools import partial
def f(a1, a2, a3):
return a1 * a2 * a3
partial(f, 3, 2)(4)
OUTPUT:
24
partial(f, 3)(2, 4)
OUTPUT:
24
partial(partial(f, 3), 2)(4)
OUTPUT:
24
partial(f, 3)(2, 4)
OUTPUT:
24
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Using partial
The function partial
of the module functools
of Python allows you to create partially applied functions. A partially applied function is a new function that is derived from an existing function by fixing a certain number of arguments in advance. The result is a function that can be called with the remaining arguments. We can use it for the commposition of functions.
We demonstrate the way of working in the following examples:
Decorator for currying
def f(x, y, z):
return x * y * z
The following Python function curry
is a higher-order function that transforms a regular function into a curried function. The curried function returns a new function for each argument provided, and you can apply these functions one at a time, which can be useful in various scenarios.
Inside the curry function, a nested function curried is defined. It accepts a variable number of arguments using *args. This allows the curried function to accept any number of arguments.
print(f'{args}')
: simply prints the current arguments to the console. It's not necessary for the functionality of course, it's included for debugging and illustration purposes.
if len(args) == func.__code__.co_argcount
: This condition checks whether the number of arguments provided (len(args)) matches the number of arguments that the original function func expects
. The co_argcount
attribute is used to retrieve the number of arguments of the func function.
return func(*args)
: If the number of arguments matches, the original function func
is called with the provided arguments, and the result is returned.
else: return lambda x: curried(*(args + (x,)))
If the number of arguments doesn't match, a new lambda function is returned. This lambda function takes a single argument x, and it calls the curried function with the existing args and the new argument x appended. This effectively allows you to build up (accumulate) the arguments one by one and create a chain of unary functions.
The co_argcount
sub-attribute returns the number of positional arguments (including arguments with default values). To learn more about the __code__
-attribute you can consult our page Argument Count for Advanced Python Programmers
def curry(func):
def curried(*args):
print(f'{args}')
if len(args) == func.__code__.co_argcount:
return func(*args)
else:
return lambda x: curried(*(args + (x,)))
return curried
@curry
def prod3(x, y, z):
return x + y + z
prod3(3)(4)(5)
OUTPUT:
(3,) (3, 4) (3, 4, 5) 12
Another exaple for using our curry
function. We first repeat the curry
without the debugging print:
def curry(func):
def curried(*args):
if len(args) == func.__code__.co_argcount:
return func(*args)
else:
return lambda x: curried(*(args + (x,)))
return curried
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Another Example for Currying
Let's consider a function for calculating the total cost of a shopping cart, with the ability to apply various discounts at different stages. This example will use multiple curried functions to apply item prices, quantities, and discounts incrementally.
First, we define a curried function for calculating the cost of individual items:
@curry
def calculate_item_cost(price, quantity):
return price * quantity
Next, we create a function to apply a percentage discount to the cost. This function takes a percentage and returns a curried function:
@curry
def subtract_percentage(discount_percentage, cost):
#print(f"{cost=}, {discount_percentage=}")
discount_amount = (cost * discount_percentage) / 100
return cost - discount_amount
For the following, it might be useful to be familiar with to simple forms of price reduction in the business world:
Rebates and discounts are two distinct pricing strategies that businesses use to incentivize customers. The main distinction between discounts and rebates lies in the timing of the price reduction. Discounts provide immediate cost savings at the point of sale, while rebates offer savings after a purchase has been made, contingent on meeting specific criteria. Both strategies can be effective in influencing customer behavior and boosting sales, but they are suited to different business scenarios and objectives.
Now, let's use these curried functions to calculate the total cost of a customers order list in the end:
# Calculate the cost of individual items
item1 = calculate_item_cost(10)(2) # Item 1 costs € 10 with a quantity of 2
item2 = calculate_item_cost(5)(3) # Item 2 costs € 5 with a quantity of 3
print(f"{item1=}, {item2=}")
# Apply percentage discounts
calculate_discounted_price = subtract_percentage(12)
calculate_rebated_price = subtract_percentage(3)
# Calculate the total cost with discounts
total_cost = calculate_rebated_price(calculate_discounted_price(item1 + item2))
print(f"Total cost: €{total_cost:.2f}")
OUTPUT:
item1=20, item2=15 Total cost: €29.88
We could have done the calculation directly on each item:
subtract_percentage(3)(subtract_percentage(12)(calculate_item_cost(10)(2)))
OUTPUT:
17.072000000000003
In the previous example we used the same rebate and discount on each item. Usually, they differ from product to product.
Let's assume that we have the following dictionary as a shopping list for items. Each item consists of a tuple with (quantity, price, discount, rebate). We can do the summation in a clean and readable way:
shopping_list = [(2, 10, 12, 3), (3, 5, 12, 3)]
total = 0
sub_perc = subtract_percentage # just a shorter name
for quantity, price, discount, rebate in shopping_list:
subtotal = sub_perc(rebate)(sub_perc(discount)((calculate_item_cost(price)(quantity))))
#print(subtotal)
total += subtotal
print(f"Total cost: €{total:.2f}")
OUTPUT:
Total cost: €29.88
Currying Function with an Arbitrary Number of Arguments
One interesting question remains: How to curry a function across an arbitrary and unknown number of parameters?
We can use a nested function to make it possible to "curry" (accumulate) the arguments. We will need a way to tell the function calculate and return the value. If the funtions is called with arguments, these will be curried, as we have said. What if we call the function without any arguments? Right, this is a fantastic way to tell the function that we finally want to the the result. We can also clean the lists with the accumulated values:
def arimean(*args):
return sum(args) / len(args)
def curry(func):
# to keep the name of the curried function:
curry.__curried_func_name__ = func.__name__
f_args, f_kwargs = [], {}
def f(*args, **kwargs):
nonlocal f_args, f_kwargs
if args or kwargs:
f_args += args
f_kwargs.update(kwargs)
return f
else:
result = func(*f_args, *f_kwargs)
f_args, f_kwargs = [], {}
return result
return f
curried_arimean = curry(arimean)
curried_arimean(2)(5)(9)(5)
OUTPUT:
<function __main__.curry.<locals>.f(*args, **kwargs)>
We have to call the function again with an empty argument, because otherwise it doesn't know that it's at the end. this is not "proper" currying, but it works:
curried_arimean(2)(5)(9)(5)()
OUTPUT:
5.25
arimean(2, 5, 9, 5)
OUTPUT:
5.25
Including some prints might help to understand what's going on:
def arimean(*args):
return sum(args) / len(args)
def curry(func):
# to keep the name of the curried function:
curry.__curried_func_name__ = func.__name__
f_args, f_kwargs = [], {}
def f(*args, **kwargs):
nonlocal f_args, f_kwargs
if args or kwargs:
print("Calling curried function with:")
print("args: ", args, "kwargs: ", kwargs)
f_args += args
f_kwargs.update(kwargs)
print("Currying the values:")
print("f_args: ", f_args)
print("f_kwargs:", f_kwargs)
return f
else:
print("Calling " + curry.__curried_func_name__ + " with:")
print(f_args, f_kwargs)
result = func(*f_args, *f_kwargs)
f_args, f_kwargs = [], {}
return result
return f
curried_arimean = curry(arimean)
curried_arimean(2)(5)(9)(7)
# it will keep on currying:
curried_arimean(5, 9)
print(curried_arimean())
OUTPUT:
Calling curried function with: args: (2,) kwargs: {} Currying the values: f_args: [2] f_kwargs: {} Calling curried function with: args: (5,) kwargs: {} Currying the values: f_args: [2, 5] f_kwargs: {} Calling curried function with: args: (9,) kwargs: {} Currying the values: f_args: [2, 5, 9] f_kwargs: {} Calling curried function with: args: (7,) kwargs: {} Currying the values: f_args: [2, 5, 9, 7] f_kwargs: {} Calling curried function with: args: (5, 9) kwargs: {} Currying the values: f_args: [2, 5, 9, 7, 5, 9] f_kwargs: {} Calling arimean with: [2, 5, 9, 7, 5, 9] {} 6.166666666666667
def arimean(*args):
return sum(args) / len(args)
def curry(func):
f_args, f_kwargs = [], {}
def f(*args, **kwargs):
nonlocal f_args, f_kwargs
if args or kwargs:
f_args += args
f_kwargs.update(kwargs)
return f
else:
result = func(*f_args, **f_kwargs)
f_args, f_kwargs = [], {}
return result
return f
curried_arimean = curry(arimean)
result = curried_arimean(2)(5)(9)(7)(5, 9)()
print("Result:", result)
We can write it also as a class
, which uses the __call__
method:
def arimean(*args):
return sum(args) / len(args)
class Curry:
def __init__(self, func, *args):
self.func = func
self.args = args
def __call__(self, *args):
print(args, self.args)
if not args:
return self.func(*self.args)
return Curry(self.func, *(self.args + args))
curried_arimean = Curry(arimean)
result = curried_arimean(2)(5)(9)(7)(5, 9)
print("Result:", result, type(result))
OUTPUT:
(2,) () (5,) (2,) (9,) (2, 5) (7,) (2, 5, 9) (5, 9) (2, 5, 9, 7) Result: <__main__.Curry object at 0x7fdfe3c103d0> <class '__main__.Curry'>
c = curried_arimean(2)(5)(9)(7)(5)(9)()
c
OUTPUT:
(2,) () (5,) (2,) (9,) (2, 5) (7,) (2, 5, 9) (5,) (2, 5, 9, 7) (9,) (2, 5, 9, 7, 5) () (2, 5, 9, 7, 5, 9) 6.166666666666667
Live Python training
Enjoying this page? We offer live Python training courses covering the content of this site.
Upcoming online Courses