|$ curl https://forge-ai.dev/api/markdown?path=docs/python/functools
$cat docs/functools.md
updated Today·18-24 min read·published

functools

PythonfunctoolsFunctionalIntermediate🎯Free Tools
Introduction

functools provides higher-order tools: memoization, partial application, reducing iterables, decorating with metadata preserved, and single-dispatch generics.

lru_cache & cache
lru_cache.py
Python
1from functools import lru_cache, cache
2
3@lru_cache(maxsize=128)
4def fib(n: int) -> int:
5 if n < 2:
6 return n
7 return fib(n - 1) + fib(n - 2)
8
9print(fib(30), fib.cache_info())
10fib.cache_clear()
11
12@cache # 3.9+ unbounded; same as lru_cache(maxsize=None)
13def load_config(path: str) -> dict:
14 return {"path": path}
15
16# Arguments must be hashable
17# @cache on methods: self breaks caching unless frozen/slots carefully
18# Prefer module-level functions or cached_property for instances
DecoratorWhen
@cachePure functions, unbounded OK
@lru_cache(maxsize=N)Bound memory; tune N
@lru_cache(typed=True)Distinguish 1 vs 1.0

warning

Do not cache functions with side effects or mutable/global-dependent results.
partial
partial.py
Python
1from functools import partial
2import json
3
4dumps_pretty = partial(json.dumps, indent=2, sort_keys=True)
5print(dumps_pretty({"b": 1, "a": 2}))
6
7def power(base: int, exp: int) -> int:
8 return base ** exp
9
10square = partial(power, exp=2)
11cube = partial(power, exp=3)
12print(square(5), cube(3))
13
14# Great with map / callbacks
15from pathlib import Path
16read_utf8 = partial(Path.read_text, encoding="utf-8")
17print(read_utf8(Path("README.md") if Path("README.md").exists() else Path("/etc/hosts"))[:40])
reduce
reduce.py
Python
1from functools import reduce
2from operator import add, mul, or_
3
4print(reduce(add, [1, 2, 3, 4], 0))
5print(reduce(mul, [1, 2, 3, 4], 1))
6print(reduce(or_, [0b001, 0b010, 0b100]))
7
8# Prefer sum/any/all/str.join when they fit — clearer than reduce
9# reduce shines for tree folds and custom monoids
10def merge(a: dict, b: dict) -> dict:
11 return {**a, **b}
12
13print(reduce(merge, [{"a": 1}, {"b": 2}, {"a": 3}], {}))
wraps

Always use @wraps(fn) in decorators so __name__, __doc__, and introspection survive.

wraps.py
Python
1from functools import wraps
2import time
3from collections.abc import Callable
4from typing import TypeVar
5
6R = TypeVar("R")
7
8def timed(fn: Callable[..., R]) -> Callable[..., R]:
9 @wraps(fn)
10 def wrapper(*args, **kwargs) -> R:
11 t0 = time.perf_counter()
12 try:
13 return fn(*args, **kwargs)
14 finally:
15 print(f"{fn.__name__}: {time.perf_counter() - t0:.4f}s")
16 return wrapper
17
18@timed
19def work(n: int) -> int:
20 """Sum 0..n-1."""
21 return sum(range(n))
22
23print(work.__name__, work.__doc__)
24print(work(1_000_00))
singledispatch
singledispatch.py
Python
1from functools import singledispatch, singledispatchmethod
2from datetime import date
3
4@singledispatch
5def serialize(obj) -> str:
6 return repr(obj)
7
8@serialize.register
9def _(obj: list) -> str:
10 return "[" + ", ".join(serialize(x) for x in obj) + "]"
11
12@serialize.register
13def _(obj: date) -> str:
14 return obj.isoformat()
15
16@serialize.register(int)
17def _(obj: int) -> str:
18 return f"int:{obj}"
19
20print(serialize([1, date.today()]))
21
22class Negator:
23 @singledispatchmethod
24 def neg(self, arg):
25 raise NotImplementedError(type(arg))
26
27 @neg.register
28 def _(self, arg: int) -> int:
29 return -arg
30
31 @neg.register
32 def _(self, arg: bool) -> bool:
33 return not arg
34
35print(Negator().neg(5), Negator().neg(True))
cached_property
cached_property.py
Python
1from functools import cached_property
2
3class Report:
4 def __init__(self, path: str) -> None:
5 self.path = path
6
7 @cached_property
8 def text(self) -> str:
9 print("loading")
10 return open(self.path, encoding="utf-8").read() if False else "body"
11
12 @cached_property
13 def lines(self) -> list[str]:
14 return self.text.splitlines()
15
16r = Report("x")
17print(r.lines, r.lines) # loading once
18
19# Clear cache:
20# del r.text
📝

note

cached_property is not thread-safe by default for the first compute — fine for typical single-threaded use.
total_ordering & Others
misc.py
Python
1from functools import total_ordering, cmp_to_key
2
3@total_ordering
4class Version:
5 def __init__(self, s: str) -> None:
6 self.parts = tuple(int(x) for x in s.split("."))
7 def __eq__(self, other: object) -> bool:
8 if not isinstance(other, Version):
9 return NotImplemented
10 return self.parts == other.parts
11 def __lt__(self, other: object) -> bool:
12 if not isinstance(other, Version):
13 return NotImplemented
14 return self.parts < other.parts
15
16print(sorted([Version("1.10"), Version("1.2"), Version("1.9")]))
17
18# cmp_to_key adapts old cmp functions to key=
19def cmp(a, b):
20 return (a > b) - (a < b)
21
22print(sorted([3, 1, 2], key=cmp_to_key(cmp)))
Production Patterns
  • Cache at clear boundaries (parsed config, expensive pure transforms)
  • Expose cache_clear in tests and on config reload
  • partial for binding dependency-injected helpers
  • wraps on every decorator you ship
  • singledispatch for open extension without giant if/elif
pattern.py
Python
1from functools import lru_cache, wraps
2from collections.abc import Callable
3
4def memoize_method(fn):
5 """Per-instance LRU via hidden cache attribute."""
6 attr = f"_cache_{fn.__name__}"
7 @wraps(fn)
8 def wrapper(self, *args):
9 cache = getattr(self, attr, None)
10 if cache is None:
11 cache = lru_cache(maxsize=64)(lambda *a: fn(self, *a))
12 setattr(self, attr, cache)
13 return cache(*args)
14 return wrapper
update_wrapper & WRAPPER_ASSIGNMENTS
update_wrapper.py
Python
1from functools import update_wrapper, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES
2
3def decorator(fn):
4 def wrapper(*args, **kwargs):
5 return fn(*args, **kwargs)
6 update_wrapper(wrapper, fn)
7 # wraps is sugar for this
8 print(WRAPPER_ASSIGNMENTS) # __module__, __name__, __qualname__, __doc__, __annotations__
9 print(WRAPPER_UPDATES) # __dict__
10 return wrapper
partialmethod
partialmethod.py
Python
1from functools import partialmethod
2
3class Cell:
4 def __init__(self):
5 self._alive = False
6 def set_state(self, state: bool) -> None:
7 self._alive = state
8 set_alive = partialmethod(set_state, True)
9 set_dead = partialmethod(set_state, False)
10
11c = Cell()
12c.set_alive()
13assert c._alive is True
14c.set_dead()
15assert c._alive is False
Decision Guide
GoalTool
Memoize pure fn@cache / @lru_cache
Memoize attributecached_property
Bind argspartial / partialmethod
Preserve decorator metawraps
Type-based overloads at runtimesingledispatch
Fold sequencereduce (or sum/join)
Fill comparison methodstotal_ordering
$Blueprint — Engineering Documentation·Section ID: PYTHON-FUNCTOOLS·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.