functools.lru_cache Confuses 1 and True in Some Calls, Not Others (tested)

Python's 1 == True and hash(1) == hash(True), so a dict can't tell them apart as keys. functools.lru_cache builds its cache key from a dict, but only sometimes -- a one-argument call with a plain int or str takes an internal fast path that skips the part of the logic where that collision happens. We measured exactly when two different-looking calls land in the same cache slot and silently return the wrong-typed result.

1. Two arguments: int and bool calls share one cache entry

import functools

@functools.lru_cache()
def g(x, y):
    print("MISS, computing for", repr(x), repr(y))
    return (type(x).__name__, type(y).__name__)

print(g(1, 0))
print(g(True, False))
print(g.cache_info())
MISS, computing for 1 0
('int', 'int')
('int', 'int')
CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)

Measured: g(True, False) never runs the function body -- no second "MISS" line -- and returns ('int', 'int'), the result computed for g(1, 0). cache_info() confirms it: one miss, one hit, one entry total. If g's return value actually depended on the argument types (as in this toy example), the cached answer is wrong for the bool call, with no exception and no warning.

2. One argument, plain int vs bool: no collision at all

import functools

@functools.lru_cache()
def f(x):
    print("MISS, computing for", repr(x), type(x).__name__)
    return "result-for-" + type(x).__name__

print(f(1))
print(f(True))
print(f.cache_info())
MISS, computing for 1 int
MISS, computing for True bool
CacheInfo(hits=0, misses=2, maxsize=128, currsize=2)

Measured: with exactly one positional argument, f(1) and f(True) are two clean misses, two separate cache entries -- the opposite of experiment 1, even though 1 == True here too. CPython's functools.lru_cache has a fast path for the single-argument, untyped case: if that one argument's type is exactly int or exactly str, the raw value is used as the dict key directly, bypassing the tuple-wrapping step where equal-but-different-type values collapse into one key. bool is a subclass of int but is not exactly int, so it never takes that fast path -- it, and every other case (two or more arguments, or a single non-int/str argument like float), goes through the normal path where Python dict semantics (1 == True, equal hash) merge the keys.

3. The fix: typed=True

import functools

@functools.lru_cache(typed=True)
def g(x, y):
    print("MISS, computing for", repr(x), repr(y))
    return (type(x).__name__, type(y).__name__)

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

Measured: typed=True folds each argument's exact type into the cache key, so (1, 0) and (True, False) become distinct entries -- two misses, no wrong-typed hit. It costs one extra cache slot per distinct type combination actually seen, which is rarely significant.

Frequently asked questions

Does functools.lru_cache treat 1 and True as the same cached call?

Sometimes. We measured a two-argument cached function where g(1, 0) and g(True, False) shared one cache entry -- the second call returned the first call's cached result without re-running the function. A one-argument function called with 1 then True did not collide.

Why did the one-argument case not collide but the two-argument case did?

CPython's lru_cache has a fast path: with exactly one positional argument whose type is exactly int or str, it uses that value directly as the dict key, skipping the step that would merge it with equal-but-different-type values. bool fails that exact-type check (it's a subclass, not int itself), and any call with two or more arguments always goes through the normal path, where dict equality (1 == True) merges the keys.

How do I stop functools.lru_cache from confusing int and bool arguments?

Pass typed=True to lru_cache. We measured this making g(1, 0) and g(True, False) into two separate cache entries, each computed fresh, at the cost of one extra cache slot per distinct type combination.

Is this specific to bool and int, or does it affect other types too?

The same mechanism applies to any two values that are == and have equal hash but different types, such as int 1 and float 1.0, whenever the call doesn't take the single-argument int/str fast path -- we measured this with int/bool across a two-argument call.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in functools.lru_cache: called a two-argument cached function with (1, 0), then with (True, False), and separately called a one-argument cached function with 1 then True, comparing cache hits/misses and the values returned via cache_info() and explicit print statements inside the wrapped function. PlainHeaders is our extension; Python's functools module is built in.