functools.partial¶
partial freezes some arguments of a function, creating a new callable with fewer parameters. It's the standard way to do partial function application in Python.
Mental Model
partial is like setting default values from the outside: you take a general function, lock in one or more arguments, and get back a simpler, specialized version. Unlike lambda, the resulting partial object preserves the original function's identity and is easier to inspect and pickle.
python
from functools import partial
Basic Usage¶
```python from functools import partial
def power(base, exponent): return base ** exponent
Freeze keyword argument¶
square = partial(power, exponent=2) cube = partial(power, exponent=3)
print(square(5)) # 25 print(cube(5)) # 125
Freeze positional argument¶
double = partial(pow, 2) # pow(2, x) print(double(8)) # 256 (2^8) print(double(10)) # 1024 (2^10) ```
How partial Works¶
partial(func, *args, **kwargs) returns a new callable that:
- Prepends
argsto the positional arguments - Merges
kwargswith keyword arguments (call-time kwargs override)
```python from functools import partial
def greet(greeting, name, punctuation="!"): return f"{greeting}, {name}{punctuation}"
Freeze 'greeting' positionally¶
hello = partial(greet, "Hello") print(hello("Alice")) # Hello, Alice! print(hello("Bob", punctuation=".")) # Hello, Bob.
Freeze keyword argument¶
casual = partial(greet, punctuation="~") print(casual("Hey", "Alice")) # Hey, Alice~ ```
Inspecting partial Objects¶
```python from functools import partial
def power(base, exponent): return base ** exponent
square = partial(power, exponent=2)
print(square.func) #
double = partial(pow, 2)
print(double.func) #
partial vs lambda¶
```python from functools import partial
These produce the same result:¶
f1 = partial(pow, 2) f2 = lambda x: pow(2, x)
print(f1(10)) # 1024 print(f2(10)) # 1024 ```
Why Prefer partial¶
| Feature | partial |
lambda |
|---|---|---|
| Readability | Clear intent | Can be cryptic |
| Introspection | .func, .args, .keywords |
Opaque |
| Pickling | Picklable | Not picklable |
| Performance | Slightly faster | Slight overhead |
| Late binding | No — arguments frozen at creation | Yes — evaluated at call time |
```python from functools import partial
Late binding gotcha with lambda¶
funcs = [lambda x: x + i for i in range(3)] print([f(0) for f in funcs]) # [2, 2, 2] — all use i=2!
partial avoids this¶
funcs = [partial(lambda x, i: x + i, i=i) for i in range(3)] print([f(0) for f in funcs]) # [0, 1, 2] — correct! ```
When lambda Is Better¶
```python
Complex transformations that aren't simple argument freezing¶
transform = lambda x: x.strip().lower().replace(" ", "_")
Conditional logic¶
process = lambda x: x * 2 if x > 0 else 0
These can't be expressed cleanly with partial¶
```
Practical Examples¶
Customizing Built-in Functions¶
```python from functools import partial
Custom print¶
debug_print = partial(print, "[DEBUG]", end="\n\n") debug_print("Starting process")
[DEBUG] Starting process¶
error_print = partial(print, "[ERROR]", flush=True) error_print("Something failed")
[ERROR] Something failed¶
Custom int parsing¶
parse_binary = partial(int, base=2) parse_hex = partial(int, base=16)
print(parse_binary("1010")) # 10 print(parse_hex("FF")) # 255 ```
Sorting with Fixed Keys¶
```python from functools import partial from operator import itemgetter
Sort list of dicts by specific key¶
sort_by_name = partial(sorted, key=itemgetter('name')) sort_by_age = partial(sorted, key=itemgetter('age'))
users = [ {'name': 'Charlie', 'age': 30}, {'name': 'Alice', 'age': 25}, {'name': 'Bob', 'age': 35}, ]
print(sort_by_name(users))
[{'name': 'Alice', ...}, {'name': 'Bob', ...}, {'name': 'Charlie', ...}]¶
print(sort_by_age(users))
[{'name': 'Alice', ...}, {'name': 'Charlie', ...}, {'name': 'Bob', ...}]¶
```
Callback Registration¶
```python from functools import partial
def handle_event(event_type, data, timestamp=None): print(f"[{event_type}] {data} at {timestamp}")
Register specialized handlers¶
on_click = partial(handle_event, "CLICK") on_hover = partial(handle_event, "HOVER") on_submit = partial(handle_event, "SUBMIT")
on_click("Button A", timestamp="12:00")
[CLICK] Button A at 12:00¶
```
Configuring API Clients¶
```python from functools import partial import json
def api_request(method, url, headers=None, timeout=30): """Simulated API request.""" return f"{method} {url} (timeout={timeout})"
Create pre-configured request functions¶
auth_headers = {"Authorization": "Bearer TOKEN"} api_get = partial(api_request, "GET", headers=auth_headers) api_post = partial(api_request, "POST", headers=auth_headers) fast_get = partial(api_get, timeout=5)
print(api_get("/users"))
GET /users (timeout=30)¶
print(fast_get("/health"))
GET /health (timeout=5)¶
```
Mathematical Functions¶
```python from functools import partial import math
Logarithms with fixed base¶
log2 = partial(math.log, base=2) # Actually: math.log(x, base=2) log10 = partial(math.log, base=10)
Note: math.log signature is log(x, base=e)¶
So we need keyword argument for base¶
log2 = partial(math.log, 2) # This is log(2, x) — wrong!
Better approach for positional-only built-ins:¶
log2 = lambda x: math.log(x, 2) # Use lambda here log10 = lambda x: math.log(x, 10)
Rounding with fixed precision¶
round2 = partial(round, ndigits=2) round4 = partial(round, ndigits=4)
print(round2(3.14159)) # 3.14 print(round4(3.14159)) # 3.1416 ```
partialmethod — For Class Methods¶
partialmethod works like partial but is designed for use inside class definitions:
```python from functools import partialmethod
class Connection: def init(self): self._connected = False
def set_state(self, state):
self._connected = state
print(f"Connected: {self._connected}")
connect = partialmethod(set_state, True)
disconnect = partialmethod(set_state, False)
conn = Connection() conn.connect() # Connected: True conn.disconnect() # Connected: False ```
Why Not Use Regular partial¶
```python class Broken: def set_state(self, state): self._state = state
# This doesn't work — partial doesn't know about 'self'
# connect = partial(set_state, True) # TypeError!
# partialmethod handles the descriptor protocol correctly
connect = partialmethod(set_state, True) # Works!
```
Chaining partial¶
```python from functools import partial
def request(method, url, headers=None, timeout=30, retries=1): return f"{method} {url} (timeout={timeout}, retries={retries})"
Build up configuration in layers¶
authenticated = partial(request, headers={"Auth": "token"}) fast_auth = partial(authenticated, timeout=5) resilient_fast_auth = partial(fast_auth, retries=3)
print(resilient_fast_auth("GET", "/api/data"))
GET /api/data (timeout=5, retries=3)¶
```
Note: Later keyword arguments override earlier ones:
```python from functools import partial
def func(a=1, b=2, c=3): return (a, b, c)
p1 = partial(func, a=10, b=20) p2 = partial(p1, b=99) # Overrides b=20
print(p2()) # (10, 99, 3) ```
Use with map, filter, sorted¶
```python from functools import partial
With map¶
numbers = [1, 2, 3, 4, 5] multiply_by_3 = partial(lambda x, factor: x * factor, factor=3) result = list(map(multiply_by_3, numbers)) print(result) # [3, 6, 9, 12, 15]
Simpler with operator¶
from operator import mul triple = partial(mul, 3) result = list(map(triple, numbers)) print(result) # [3, 6, 9, 12, 15]
With filter¶
is_greater_than_3 = partial(lambda threshold, x: x > threshold, 3) result = list(filter(is_greater_than_3, numbers)) print(result) # [4, 5]
With sorted¶
data = ["banana", "apple", "cherry"] sort_by_length = partial(sorted, key=len) print(sort_by_length(data)) # ['apple', 'banana', 'cherry'] ```
Common Pitfalls¶
Positional Argument Order¶
```python from functools import partial
def divide(a, b): return a / b
This fixes 'a', not 'b'¶
half = partial(divide, 2) # divide(2, x) → 2/x, not x/2! print(half(10)) # 0.2 (not 5.0!)
To fix 'b', use keyword argument¶
half = partial(divide, b=2) # divide(x, b=2) → x/2 print(half(10)) # 5.0 ```
Keyword Override Silently Replaces¶
```python from functools import partial
def func(a, b, c): return (a, b, c)
p = partial(func, b=10) print(p(1, c=3)) # (1, 10, 3) print(p(1, b=99, c=3)) # (1, 99, 3) — b=10 silently overridden ```
Summary¶
| Feature | Details |
|---|---|
| Import | from functools import partial |
| Purpose | Freeze arguments to create specialized functions |
| Attributes | .func, .args, .keywords |
| Class variant | partialmethod for methods in class bodies |
| Picklable | Yes (unlike lambda) |
Key Takeaways:
partialfreezes positional and keyword arguments, creating a simpler callable- Prefer
partialoverlambdafor simple argument freezing — it's more readable and inspectable - Use
partialmethodinside class definitions whereselfis involved - Watch positional argument order —
partial(f, x)always fills the first positional argument - Later keyword arguments override earlier ones when chaining partial objects
partialobjects are picklable, making them suitable for multiprocessing
Exercises¶
Exercise 1.
Write a power(base, exponent) function. Use functools.partial to create square, cube, and fourth_power functions. Verify each with the input 5.
Solution to Exercise 1
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
fourth_power = partial(power, exponent=4)
print(square(5)) # 25
print(cube(5)) # 125
print(fourth_power(5)) # 625
Exercise 2.
Use partial to create a family of string formatting functions from a single format_string(template, value) function. Create format_currency (template "${:.2f}"), format_percent (template "{:.1%}"), and format_scientific (template "{:.2e}"). Test each.
Solution to Exercise 2
from functools import partial
def format_string(template, value):
return template.format(value)
format_currency = partial(format_string, "${:.2f}")
format_percent = partial(format_string, "{:.1%}")
format_scientific = partial(format_string, "{:.2e}")
print(format_currency(1234.5)) # $1234.50
print(format_percent(0.856)) # 85.6%
print(format_scientific(0.00042)) # 4.20e-04
Exercise 3.
Demonstrate the difference between partial and lambda for argument binding. Create both partial_double = partial(mul, 2) and lambda_double = lambda x: mul(2, x). Print the func, args, and keywords of the partial object and show that the lambda has no such attributes. Verify both produce the same results.
Solution to Exercise 3
from functools import partial
from operator import mul
partial_double = partial(mul, 2)
lambda_double = lambda x: mul(2, x)
# partial exposes its internals
print(partial_double.func) # <built-in function mul>
print(partial_double.args) # (2,)
print(partial_double.keywords) # {}
# lambda has no such attributes
print(hasattr(lambda_double, 'func')) # False
# Both produce the same results
print(partial_double(5)) # 10
print(lambda_double(5)) # 10