The Python Coding Stack • by Stephen Gruppetta

Share this post

Python's functools.partial() Lets You Pre-Fill A Function

thepythoncodingstack.substack.com

Discover more from The Python Coding Stack • by Stephen Gruppetta

I write the articles I wish I had when I was learning Python programming I learn through narratives, stories. And I communicate in the same way, with a friendly and relaxed tone, clear and accessible
Over 1,000 subscribers
Continue reading
Sign in

Python's functools.partial() Lets You Pre-Fill A Function

An exploration of partial() and partialmethod() in functools

Stephen Gruppetta
May 7, 2023
9
Share this post

Python's functools.partial() Lets You Pre-Fill A Function

thepythoncodingstack.substack.com
Share

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:

import random  for _ in range(10):     if random.random() < 0.5:         print("Heads", end=" ")     else:         print("Tails", end=" ")
copy code

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:

import functools import random  print_on_line = functools.partial(print, end=" ")  for _ in range(10):     if random.random() < 0.5:         print_on_line("Heads")     else:         print_on_line("Tails")
copy code

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:

  1. The name of the function you want to "pre-fill", which is print

  2. The keyword argument end=" ". This is also a valid keyword argument in print()

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:

>>> import functools  >>> print_on_line = functools.partial(print, end=" ") >>> type(print_on_line) <class 'functools.partial'>  >>> callable(print_on_line) True
copy code

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:

>>> import functools  >>> print_on_line = functools.partial(print, end=" ")  >>> print_on_line.func <built-in function print>  >>> print_on_line.args ()  >>> print_on_line.keywords {'end': ' '}
copy code

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:

import random  def roll_dice(max_value, number_of_dice):     return [         random.randint(1, max_value)         for _ in range(number_of_dice)     ]  # Roll two six-sided dice print(roll_dice(6, 2))  # Roll four ten-sided dice print(roll_dice(10, 4))
copy code

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 dice

  • number_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:

import functools import random  def roll_dice(max_value, number_of_dice):     return [         random.randint(1, max_value)         for _ in range(number_of_dice)     ]  # Create a partial object that always rolls # standard dice with 6 sides roll_standard_dice = functools.partial(roll_dice, 6)  # Roll four standard dice print(roll_standard_dice(4))  # Roll two standard dice print(roll_standard_dice(2))
copy code

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:

import functools import random  def roll_dice(max_value, number_of_dice):     return [         random.randint(1, max_value)         for _ in range(number_of_dice)     ]  # Create a partial object that always rolls # standard dice with 6 sides roll_standard_dice = functools.partial(roll_dice, 6)  print(roll_standard_dice.func) # <function roll_dice at 0x102ab0720>  print(roll_standard_dice.args) # (6,)  print(roll_standard_dice.keywords) # {}
copy code

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:

import functools import random  def roll_dice(max_value, number_of_dice):     return [         random.randint(1, max_value)         for _ in range(number_of_dice)     ]  # Create a partial object that always rolls # two standard dice with 6 sides roll_monopoly_dice = functools.partial(roll_dice, 6, 2)  print(roll_monopoly_dice.func) # <function roll_dice at 0x102ab0720>  print(roll_monopoly_dice.args) # (6, 2)  print(roll_monopoly_dice.keywords) # {}  print(roll_monopoly_dice()) # [5, 1]
copy code

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():

functools.partial(func, /, *args, **keywords)

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:

import functools import random  bizarre_print = functools.partial(     print,     "Here is a random number",     random.randint(0, 100),     sep=" • | • ",     end=" <THE END>\n", )  bizarre_print("Hello", "Let's try this out")
copy code

Try to predict the output before running the code or reading on.

The functools.partial() call creating bizarre_print has five arguments:

  1. print is the first positional argument in functools.partial(), which is assigned to func

  2. The string "Here is a random number" is the second positional argument, but the first one to form part of *args

  3. The random integer returned by random.randint() is the third positional argument. This is also assigned to the args tuple

  4. The following argument is sep=" • | • ". This is a keyword argument and is assigned to the keywords dictionary

  5. There's one final keyword argument, end=" <THE END>\n", which is also in keywords

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:

# 'bizarre_print("Hello", "Let's try this out")' is the same as… print(     "Here is a random number",     random.randint(0, 100),     "Hello",     "Let's try this out",     sep=" • | • ",     end=" <THE END>\n", )
copy code

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:

import functools import random  bizarre_print = functools.partial(     print,     "Here is a random number",     random.randint(0, 100),     sep=" • | • ",     end=" <THE END>\n", )  print(bizarre_print.args) # ('Here is a random number', 42)  print(bizarre_print.keywords) # {'sep': ' • | • ', 'end': ' <THE END>\n'}
copy code

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:

import functools  class Article:     def set_platform(self, platform):         self.platform = platform      # This won't work! We'll fix it soon     set_substack = functools.partial(       set_platform,       'substack',     )  article = Article() article.set_substack() print(article.platform)
copy code

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":

import functools  class Article:     def set_platform(self, platform):         self.platform = platform      # This still won't work! We'll fix it soon     set_substack = functools.partial(         set_platform,         platform='substack',     )  article = Article() article.set_substack() print(article.platform)
copy code

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():

import functools  class Article:     def set_platform(self, platform):         self.platform = platform      # Using partialmethod...     set_substack = functools.partialmethod(         set_platform,         'substack',     )  article = Article() article.set_substack() print(article.platform)
copy code

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:

import functools  class Article:     def set_platform(self, platform):         self.platform = platform      # Using partialmethod...     set_substack = functools.partialmethod(         set_platform,         'substack',     )  article = Article() article.set_substack()  print(article.set_substack.func) # <bound method Article.set_platform of <__main__.Article #   object at 0x100844e90>>  print(article.set_substack.args) # ('substack',)  print(article.set_substack.keywords) # {}
copy code

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


The Python Coding Stack • by Stephen Gruppetta is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.


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!

9
Share this post

Python's functools.partial() Lets You Pre-Fill A Function

thepythoncodingstack.substack.com
Share
Previous
Next
Comments
Top
New
Community

No posts

Ready for more?

© 2023 Stephen Gruppetta
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing