runtime
167 / 250
lesson 167 Functional & Dynamic Python · advanced

functools.partial: fix some arguments now

partial(func, *args, **kwargs) returns a new callable with some arguments already filled in. It is the tidy way to adapt a general function to a place that expects a simpler one: a sort key, a callback, a table of converters.

You'll be able to
  • Build a partial with positional and keyword arguments, and call it with the rest
  • Use partials as sort keys and as entries in a table of functions
  • Explain why a partial built in a loop keeps its own value while a lambda in a loop may not
The idea

Many places want a function of a particular shape: sorted(key=...) wants one argument, a callback might want none, map wants one. Your function often takes more. **functools.partial(func, *args, **kwargs) returns a callable that remembers those arguments; calling it adds the new ones: `partial(func, a)(b, c=1)` runs `func(a, b, c=1)`. Its arguments are fixed when the partial is created**, and you can inspect them: .func, .args, .keywords.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Specialised converters
from functools import partial

def convert(amount, rate, fee=0.0):
    return amount * rate - fee

to_eur = partial(convert, rate=0.92)
to_gbp_card = partial(convert, rate=0.79, fee=1.5)
parse_hex = partial(int, base=16)

print(to_eur(50))
print(to_gbp_card(100))
print(to_eur(50, fee=2))
print(parse_hex("ff"), parse_hex("1A"))
print(to_eur.func.__name__, to_eur.args, to_eur.keywords)

A partial is handy wherever a function is passed around. Here one general scoring function becomes a sort key with fixed weights, and a loop builds one filter per nutrient. In a loop, partial captures each value as it is created, which is exactly where a lambda written in the loop goes wrong (the concept check shows it).

example-2.py · Sort keys and a table of filters
from functools import partial

def score(work, citations_weight, year_weight):
    return work["cited_by_count"] * citations_weight + work["publication_year"] * year_weight

works = [
    {"id": "W3003257820", "publication_year": 2020, "cited_by_count": 38040},
    {"id": "W2970971581", "publication_year": 2019, "cited_by_count": 31488},
    {"id": "W3021613070", "publication_year": 2020, "cited_by_count": 2362},
]
by_citations = partial(score, citations_weight=1, year_weight=0)
by_recent = partial(score, citations_weight=0.001, year_weight=100)
print([w["id"] for w in sorted(works, key=by_citations, reverse=True)])
print([w["id"] for w in sorted(works, key=by_recent, reverse=True)])

def above(product, nutrient, limit):
    return product["nutriments"].get(nutrient, 0) > limit

checks = {}
for nutrient, limit in {"sugars_100g": 22.5, "salt_100g": 1.5}.items():
    checks[nutrient] = partial(above, nutrient=nutrient, limit=limit)
corn_flakes = {"nutriments": {"sugars_100g": 10.71, "salt_100g": 1.785}}
print({name: check(corn_flakes) for name, check in checks.items()})

When to use it: to adapt a general function to a slot that expects fewer arguments, to build tables of configured functions, and as a clearer alternative to a lambda that only fills in arguments. Mistakes to avoid: fixing a positional argument in the wrong position (partial(convert, 0.92) sets amount, not rate: use keywords); fixing a value that should change between calls; and expecting a partial to have __name__ (it doesn't; look at .func.__name__).

Review
  • partial(func, *args, **kwargs) returns a callable with those arguments filled in; calls add the rest.
  • Fix arguments by keyword unless they really are the first positional ones.
  • A partial stores values when it is created, which avoids the late-binding trap of lambdas in loops.
  • Next: composing functions into pipelines.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

functools.partial: fix some arguments now — Functional & Dynamic Python · Runtime