Python's functools.partial() Lets You Pre-Fill A Function
An exploration of partial() and partialmethod() in functools
Don't you love it when you need to fill in a form and some fields are already filled in?
There are times when you're coding in Python when you need to use a function with the same arguments over and over again. If only you could pre-fill those arguments, like the form!
As it happens, you can. Python's functools.partial()
enables you to "pre-fill" some arguments in a function. In this article, I'll explore how to use partial()
. I'll also look at its close relative, partialmethod()
.
A quick 'author's note': Some of the articles I'll publish on The Python Coding Stack—such as this one—will be focused deep dives into a specific Python function or class, either from the standard module or from key third-party modules such as NumPy or Matplotlib. Readers will fall into one of three categories: 1. You may already know everything about this topic. 2. You may be entirely new to the topic. Or... 3. You may have used the function or class but haven't yet explored it fully. When I write deep dives, I'm usually in the third category trying to learn more about these tools myself.
Using Python's functools.partial()
: Modifying print()
When you use the built-in print()
function, the last character printed out is the newline character by default. Let's assume you'd like to use print()
often in a program, but you don't want your output to be on a new line each time. You can use the optional argument end
in print()
. I'll use a short example to demonstrate this:
The output of this code is shown below:
Tails Tails Heads Heads Tails Heads Tails Heads Tails Heads
This code "flips a coin" by generating a random number between 0 and 1 using random.random()
and printing "Heads"
if the number is lower than 0.5 and "Tails"
if it's larger.
The second argument in the two print()
calls replaces the newline character, which is the last character by default, with a space. If you omit this keyword argument, each print()
will start a new line.
If you need to use this often in a program, you may wish to have a version of the print()
function with end=" "
pre-filled. You can use functools.partial()
for this:
The output is similar to the previous example:
Heads Tails Tails Tails Heads Heads Tails Tails Tails Tails
partial()
is part of the functools
module. You call functools.partial()
with two arguments:
The name of the function you want to "pre-fill", which is
print
The keyword argument
end=" "
. This is also a valid keyword argument inprint()
partial()
returns an object you call print_on_line
. You can use this object to replace the print()
function, as we've done in the if..else
blocks in this code. The call to print_on_line("Heads")
is equivalent to print("Heads", end=" ")
. We'll look at what's happening in more detail shortly.
What Does partial()
Return?
The object returned by functools.partial()
looks and behaves like a function. It's an object of type partial
. We'll continue exploring this in a Console/REPL session:
This confirms that print_on_line
is a functools.partial
object and that it's callable. This is why we can use it in the same way as the function we're "pre-filling".
The partial
object has three attributes:
.func
.args
.keywords
Let's see their values in this example:
The .func
attribute contains a reference to the built-in print
function. This is the pre-existing function we're building on. The .args
attribute shows any positional arguments passed to partial()
. In this case, there aren't any since the only additional argument is a keyword argument. We wrote the argument as the named keyword argument end=" "
. If we wrote just " "
instead, the argument would be a positional argument.
We'll look at another example later that will include positional arguments. You can read more about positional and keyword arguments in this article I wrote in the pre-substack era!
The final attribute, .keywords
, shows any keyword arguments passed to partial()
. This is a dictionary with the key "end"
with the empty string " "
as its value.
Any positional and keyword arguments passed to partial()
are automatically passed to the original function. This is why print_on_line("Heads")
is equivalent to print("Heads", end=" ")
.
Using partial()
With User-Defined Functions
Let's use another short example to further explore functools.partial()
. This function rolls several dice. The dice can have any number of sides:
This code's two print()
functions output the following lists:
[5, 1]
[3, 7, 10, 1]
You'll almost certainly get a different output as the numbers are random. The function roll_dice()
takes two arguments:
max_value
determines the number of sides of the dicenumber_of_dice
determines how many dice to roll
I split the list comprehension into multiple lines for display purposes only.
Therefore, roll_dice(6, 2)
rolls two six-sided dice. The output is a list with two values, with the values between 1 and 6. In the second call, roll_dice(10, 4)
, there are four dice, each with ten sides.
Now, let's create a partial
object, roll_standard_dice
, which we can use instead of roll_dice()
for the case when we're using standard six-sided dice:
The output is:
[1, 5, 3, 2]
[6, 1]
We passed the function roll_dice
as the first argument to functools.partial()
. The second argument is a positional argument since we only passed the value 6 without using a keyword to name it.
Therefore, the expression roll_standard_dice(4)
is equivalent to calling roll_dice(6, 4)
. The positional argument we use in functools.partial()
, which is 6 in this case, is used to "pre-fill" the first positional argument in roll_dice()
. The argument you pass to the partial
object, roll_standard_dice()
, is passed on to roll_dice()
as its second positional argument. This value is 4 in the first call and 2 in the second.
Let's look at the three attributes of this partial
object. I'm showing the outputs as comments within the code in this case for better clarity:
The attribute roll_standard_dice.func
references the function we're building on, roll_dice
. The attribute .args
contains a tuple with one value, 6, since we passed one positional argument to functools.partial()
.
Since no keyword arguments are passed to functools.partial()
, the .keywords
attribute is an empty dictionary.
And if you're playing Monopoly, in which you always roll two six-sided dice, you can create another partial
object. I'm showing the outputs as comments again in this code segment:
We passed two positional arguments to functools.partial()
when creating roll_monopoly_dice
, 6 and 2. Therefore, this partial
object is equivalent to roll_dice(6, 2)
.
One Final Look At functools.partial()
Let's look at the signature for functools.partial()
:
You pass the name of the function you would like to "pre-fill" as the first positional argument in functools.partial()
. The forward slash /
in the signature forces the first argument to be positional-only.
Then, you can add as many positional arguments as you wish, followed by any number of keyword arguments. You can read more about *args
and **kwargs
in this article. The positional arguments are stored in the .args
attribute as a tuple, and the keyword arguments are stored in .keywords
as a dictionary.
Let's go back to the print()
function and create a rather bizarre partial
object from it. The print()
function can take several valid keyword arguments, which include end
and sep
:
Try to predict the output before running the code or reading on.
The functools.partial()
call creating bizarre_print
has five arguments:
print
is the first positional argument infunctools.partial()
, which is assigned tofunc
The string
"Here is a random number"
is the second positional argument, but the first one to form part of*args
The random integer returned by
random.randint()
is the third positional argument. This is also assigned to theargs
tupleThe following argument is
sep=" • | • "
. This is a keyword argument and is assigned to thekeywords
dictionaryThere's one final keyword argument,
end=" <THE END>\n"
, which is also inkeywords
Here's the output from the code above:
Here is a random number • | • 20 • | • Hello • | • Let's try this out <THE END>
We've seen that the end
parameter in print()
determines the last printed character. The sep
argument is used to separate multiple values. Its default value is a space, but we've replaced the default with the string " • | • "
.
Therefore, bizarre_print("Hello", "Let's try this out")
is equivalent to this print()
call:
The two positional arguments passed in functools.partial()
are passed to print()
first. The positional arguments passed to bizarre_print()
come next in the equivalent print()
call. Finally, there are the two keyword arguments from the partial
object. You could also pass additional keyword arguments in bizarre_print()
if you wish.
Let's look at the .args
and .keywords
attributes in bizarre_print
:
Now we have two positional arguments in .args
and two keyword arguments in .keywords
. These are passed to print()
along with any other positional and keyword arguments in bizarre_print()
.
Does This Work With Methods?
A method is a function. It's a function that's part of a class, but it's still a function. So can we use the same tools to pre-fill arguments in a method? Let's try this out by creating a class and using functools.partial()
on a method. You'll see that we'll encounter a problem:
This raises the following error:
Traceback (most recent call last):
File ... line 14, in <module>
article.set_substack()
TypeError: Article.set_platform() missing 1 required positional argument: 'platform'
The class has a method called set_platform()
. This method sets the platform used to publish the article. But since most of my articles are now on Substack (!), we tried to create a partial
object called set_substack
with the platform
article pre-set to "substack"
.
This raises an error. The first parameter in set_platform()
is self
. Therefore, the string "substack"
we used in functools.partial()
is passed to set_platform()
as its first argument. However, the first argument should be a reference to the instance itself. When you call a method in the usual way, such as if you use article.set_platform()
, the reference to the instance is passed to the method automatically. But in this case, we're calling article.set_substack()
, and set_substack
is not a method but the object returned by partial()
.
As a result, the required parameter platform
is unfilled. This leads to the TypeError
. We can try to pass the string "substack"
to partial()
as a keyword argument instead, using platform="substack"
:
This still raises an error:
Traceback (most recent call last):
File ... line 14, in <module>
article.set_substack()
TypeError: Article.set_platform() missing 1 required positional argument: 'self'
The error message has changed. The parameter platform
is no longer the issue since we passed the value using a keyword argument. Therefore the platform
parameter in set_platform()
has a value assigned to it. However, self
is missing now!
The functools
module provides us with another tool to replicate the behaviour of partial()
for methods. This is functools.partialmethod()
:
The output now shows the name of the platform:
substack
We can look at the attributes .func
, .args
, and .keywords
for the partial
object Article.set_substack
. The outputs are shown as comments in this example:
The .func
attribute now references the method bound to the instance article
. We only included one positional argument in functools.partialmethod()
. Therefore, there's a tuple with one item in .args
, and .keywords
is an empty dictionary.
Final Words
Just like your pre-filled forms, you can now create "pre-filled" functions or methods using functools.partial()
and functools.partialmethod()
, which allow you to freeze some arguments. You may wish to do this to avoid repetitive function calls with the same arguments or to simplify function calls with many arguments. In some cases, this makes the code more readable, more maintainable, and less prone to bugs.
Code in this article uses Python 3.11
Stop Stack
Recently published articles on The Stack:
Let The Real Magic Begin • Creating Classes in Python. Year 2 at Hogwarts School of Codecraft and Algorithmancy • Defining Classes • Data Attributes
Sequences in Python. Sequences are different from iterables • Part 2 of the Data Structure Categories Series
Harry Potter and The Object-Oriented Programming Paradigm. Year 1 at Hogwarts School of Codecraft and Algorithmancy • The Mindset
Iterable: Python's Stepping Stones. What makes an iterable iterable? Part 1 of the Data Structure Categories Series
Why Do 5 + "5" and "5" + 5 Give Different Errors in Python? • Do You Know The Whole Story? If
__radd__()
is not part of your answer, read on…
The next cohort of The Python Coding Programme starts next week. Live sessions with very small cohorts over 3 weeks, with 90 minutes live on Zoom every day (4 days a week). Each cohort only has 4 participants and there's active mentoring throughout with a private forum for the cohort to continue discussions. Here's some more information about The Python Coding Programme for Beginners.
Most articles will be published in full on the free subscription. However, a lot of effort and time goes into crafting and preparing these articles. If you enjoy the content and find it useful, and if you're in a position to do so, you can become a paid subscriber. In addition to supporting this work, you'll get access to the full archive of articles and some paid-only articles. Thank you!