Use Cases for Partials in Python That You Should Know
What are Partials? #
Partials are a way to ‘pre-fill’ some arguments of a function to produce another function with a smaller number of parameters. In Python, this can be achieved using the partial
function from the functools
module.
The syntax for a partial function is as follows:
from functools import partial
new_function = partial(original_function, arg1, arg2, ...)
For example, consider the following function that has 3 parameters:
def multiply(x, y, z):
return x * y * z
You can create a partial function that fixes the first argument:
from functools import partial
double_and_triple = partial(multiply, 2, 3)
Now, double_and_triple
is a new function that takes a single argument and multiplies it by 2 and 3:
result = double_and_triple(4) # Equivalent to multiply(2, 3, 4)
print(result) # Output: 24
This of course, is a rather contrived example, but it illustrates the concept of partial application. Here are some practical use cases for partials in Python.
Detail Logger Use Case #
Logging is usually straightforward as you typically only need to supply the log level and message as parameters. However, for more detailed logging, you might want to include additional information, such a the context that the message is for. e.g. communications, audit, etc.
Use partials to create specialized logging functions with pre-filled context.
import logging
from functools import partial
logging.basicConfig(level=logging.DEBUG)
def log_message(level, type, message):
full_message = f"[{type}] {message}"
if level == "info":
logging.info(full_message)
elif level == "debug":
logging.debug(full_message)
elif level == "warning":
logging.warning(full_message)
log_audit = partial(log_message, "info", "audit")
log_comms = partial(log_message, "debug", "comms")
log_storage = partial(log_message, "debug", "storage")
log_audit("user logged in") # INFO:root:[audit] user logged in
log_comms("connection established") # DEBUG:root:[comms] connection established
log_storage("database connected") # DEBUG:root:[storage] database connected
Validation Use Case #
Validating different fields can often result in defining multiple functions for each required validation. By using partials, you can create a generic validation function and use partials is easily create required validation functions.
from functools import partial
def validate_field(validator_type, min_val=None, max_val=None, required=True, value=None):
"""Generic field validator that can be curried for specific use cases"""
if required and value is None:
return False, "Field is required"
if value is None:
return True, "Valid"
if validator_type == "string":
if min_val and len(value) < min_val:
return False, f"String too short (min: {min_val})"
if max_val and len(value) > max_val:
return False, f"String too long (max: {max_val})"
elif validator_type == "number":
if min_val is not None and value < min_val:
return False, f"Value too small (min: {min_val})"
if max_val is not None and value > max_val:
return False, f"Value too large (max: {max_val})"
return True, "Valid"
You can then create specific validation functions using partials:
validate_username = partial(validate_field, "string", min_val=3, max_val=20)
validate_age = partial(validate_field, "number", min_val=0, max_val=120)
validate_password = partial(validate_field, "string", min_val=8, required=True)
validate_bio = partial(validate_field, "string", max_val=500, required=False)
Usage would be as follows:
is_valid, message = validate_username(value="Alice") # True, "Valid"
is_valid, message = validate_age(value=25) # True, "Valid"
Filters Use Case #
Partials are a great way to defined a number of common filters that you may want to use on lists of items.
For example, you may want to create price range functions that can be used in filter calls, like so:
from functools import partial
def filter_by_price(product, min_price=None, max_price=None):
if product is None:
return False
if min_price is not None and product["price"] < min_price:
return False
if max_price is not None and product["price"] > max_price:
return False
return True
filter_cheap = partial(filter_by_price, max_price=50)
filter_mid_range = partial(filter_by_price, min_price=50, max_price=100)
filter_expensive = partial(filter_by_price, min_price=100)
products = [
{"name": "Product 1", "price": 30},
{"name": "Product 2", "price": 70},
{"name": "Product 3", "price": 120},
{"name": "Product 4", "price": 90},
{"name": "Product 5", "price": 20},
{"name": "Product 6", "price": 150},
]
cheap_products = list(filter(filter_cheap, products))
mid_range_products = list(filter(filter_mid_range, products))
expensive_products = list(filter(filter_expensive, products))
print("Cheap Products:", cheap_products)
print("Mid-range Products:", mid_range_products)
print("Expensive Products:", expensive_products)
# output
Cheap Products: [{'name': 'Product 1', 'price': 30}, {'name': 'Product 5', 'price': 20}]
Mid-range Products: [{'name': 'Product 2', 'price': 70}, {'name': 'Product 4', 'price': 90}]
Expensive Products: [{'name': 'Product 3', 'price': 120}, {'name': 'Product 6', 'price': 150}]