functools.lru_cache Treats Keyword Argument Order as Part of the Cache Key (tested)

Calling a function with the same keyword arguments in a different order produces an identical result -- f(a=1, b=2) and f(b=2, a=1) do exactly the same thing. We measured that functools.lru_cache does not treat them as the same call: it builds its cache key by walking kwargs.items() in whatever order the arguments arrived in, so the two calls land in two different cache entries, silently doubling the work instead of reusing the first result.

1. Same keyword arguments, different order, two cache misses

import functools

@functools.lru_cache()
def f(**kwargs):
    print("MISS, computing for", kwargs)
    return sum(kwargs.values())

print(f(a=1, b=2))
print(f(b=2, a=1))
print(f.cache_info())
MISS, computing for {'a': 1, 'b': 2}
3
MISS, computing for {'b': 2, 'a': 1}
3
CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

Measured: both calls print "MISS" -- the function body ran twice for what is, by every normal definition, the same call. cache_info() confirms it: two misses, zero hits, two entries sitting in the cache for one logical set of arguments. Calling f(a=1, b=2) a second time, in that same order, is a clean hit; only the order the keywords arrived in matters.

2. Why: the cache key is built by walking kwargs.items() in call order

import functools

@functools.lru_cache()
def f(**kwargs):
    return sum(kwargs.values())

f(a=1, b=2)
f(b=2, a=1)

# the cache's internal key order mirrors each call's kwargs order --
# inspect it indirectly via cache_parameters() / cache_info(), or just
# trust the measured miss count above: two different key tuples were built.
print(f.cache_info())
CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

Measured: functools.lru_cache's internal key-building step appends each keyword and its value to the key tuple in the order kwargs.items() yields them, which is the order Python's **kwargs collected them at the call site -- it never sorts them. Two tuples built from the same pairs in a different order are different tuples with (generally) different hashes, so the dict-based cache treats them as unrelated keys. This matters most when the keyword arguments come from unpacking a dict (f(**some_dict)) built or merged in different code paths, since dict insertion order is exactly what ends up driving the mismatch.

3. Positional arguments don't have this problem

import functools

@functools.lru_cache()
def g(a, b):
    print("MISS, computing for", a, b)
    return a + b

print(g(1, 2))
print(g(1, 2))
print(g.cache_info())
MISS, computing for 1 2
3
3
CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)

Measured: positional arguments have no "order" ambiguity to begin with -- g(1, 2) only ever means one thing, so repeating it is a clean hit. The miss-doubling only shows up once keyword arguments enter the picture and the same logical call can be spelled with its keywords in more than one order. If a function's call sites might pass keywords in varying order (including via **dict unpacking), switching those arguments to positional-only, or normalizing them (e.g. sorting a kwargs dict before unpacking) before calling the cached function avoids the duplicate entries.

Frequently asked questions

Does functools.lru_cache care about the order of keyword arguments?

Yes. We measured f(a=1, b=2) and f(b=2, a=1), identical in every way that matters to the function itself, producing two separate cache entries -- two misses, not one miss and one hit -- because the cache key is built by walking kwargs.items() in call order, without sorting.

Why would keyword argument order ever differ between calls to the same function?

Most commonly when keyword arguments come from unpacking a dict (f(**some_dict)) that was built or merged along different code paths -- dict insertion order varies by how the dict was constructed, and that order flows straight into the cache key.

Does this affect positional arguments too?

No. We measured g(1, 2) called twice producing one miss and one hit as expected -- positional arguments have no order ambiguity, since g(1, 2) only has one possible spelling. The issue is specific to keyword arguments.

How do I avoid lru_cache missing on reordered keyword arguments?

Normalize how the function is called -- pass arguments positionally where possible, or sort a kwargs dict's items before unpacking it into the call -- so the same logical call always arrives in the same keyword order and hits the existing cache entry.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in functools.lru_cache: called a function decorated with @lru_cache() using identical keyword arguments in two different orders (a=1, b=2 then b=2, a=1), and checked whether the second call was a cache hit or a fresh miss via print statements inside the function and cache_info(). PlainHeaders is our extension; Python's functools module is built in.