Skip to Content
Course content

181: The functools Module: partial

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably encountered situations where you have a general-purpose function that takes several arguments, but in a specific part of your app, you find yourself passing the same two or three arguments over and over again. It's tedious, and it clutters your code.

That's exactly where functools.partial comes in. Instead of writing a wrapper function every time you want to "preset" some arguments, partial lets you create a new version of a function with some of the arguments already filled in. Think of it as a factory that produces specialized versions of a generic tool.

Setting up a generic price calculator

Let's build a simple price calculator for an e-commerce store. We need a function that handles the base price, a tax rate, and a flat shipping fee. In a real app, these would come from a database, but for now, let's keep it hard-coded.

def calculate_total(price, tax_rate, shipping_fee):
    return (price * (1 + tax_rate)) + shipping_fee

# Without partial, it's a bit wordy:
print(calculate_total(100, 0.07, 5.00)) # Total for a $100 item
print(calculate_total(250, 0.07, 5.00)) # Total for a $250 item

If I'm building a module specifically for customers in one state (let's say, a "Standard Shipping" region with a 7% tax), I don't want to keep typing 0.07 and 5.00. It's a maintenance nightmare—if the shipping fee changes to $6.00, I have to hunt down every single call site.

Reducing the noise with partial

I can use partial to "freeze" those tax and shipping arguments. This creates a new callable that only requires the price.

from functools import partial

# I'm creating a specialized version of calculate_total
calculate_standard_total = partial(calculate_total, tax_rate=0.07, shipping_fee=5.00)

# Now I only need to pass the price
print(calculate_standard_total(100)) # 112.0
print(calculate_standard_total(250)) # 272.5

Notice how calculate_standard_total behaves exactly like a function, but it remembers those keyword arguments I passed in during its creation.

Fixing a positional argument blunder

Here is where I usually trip up when I'm moving fast. I tried to create a "Free Shipping" version of the function by passing the shipping fee as a positional argument. I thought I could just skip the tax rate and jump to the shipping fee.

# MISTAKE: I wanted to keep the tax at 0.07 but make shipping 0.00
# I tried this:
calculate_free_shipping = partial(calculate_total, 0.00) 

# I expected this to be the shipping fee, but look what happens:
print(calculate_free_shipping(100, 0.07)) 

If you run that, you'll see it fails or gives a weird result. Why? Because partial fills in positional arguments from left to right. By passing 0.00, I didn't freeze the shipping fee; I accidentally froze the price as 0.00! Now the function thinks the price is 0, and when I call it with (100, 0.07), Python thinks 100 is the tax_rate and 0.07 is the shipping_fee.

The fix is simple: when you want to freeze arguments that aren't the first ones in the list, always use keyword arguments.

# CORRECTED:
calculate_free_shipping = partial(calculate_total, shipping_fee=0.00)

# Now the price and tax_rate are still flexible, but shipping is locked at 0.00
print(calculate_free_shipping(100, 0.07)) # 107.0

Why bother instead of using a lambda?

You might be thinking, "Why not just use lambda price: calculate_total(price, 0.07, 5.00)?" You can, and for tiny scripts, it's fine. But partial objects are more explicit and slightly more performant. More importantly, they are introspection-friendly; you can actually access the frozen arguments via the .args and .keywords attributes of the partial object, which is a lifesaver when debugging complex pipelines.




📋 Practical Task

Exercise: Creating Specialized API Request Helpers

Imagine you are writing a wrapper for an API. You have a generic function make_request(endpoint, method, timeout, api_key). To avoid passing the api_key and timeout every time, use functools.partial to create two specialized helpers:

  • get_user_data: A version of make_request locked to the "GET" method, a timeout of 30, and the API key "SECRET_123". It should only require the endpoint as an argument.
  • post_user_data: A version locked to the "POST" method, a timeout of 60, and the API key "SECRET_123". It should also only require the endpoint.

Requirements:

  1. Define the base make_request function that prints a string describing the request (e.g., "Requesting /profile via GET (timeout: 30, key: SECRET_123)").
  2. Implement the two partial functions.
  3. Call both helpers once to verify they output the correct details for their respective methods and timeouts.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.