An opened file is not a successful-write receipt

In the Python local-cache helper, _open_file yields a handle/error pair. An opening IOError yields a missing handle; after a successful open, ordinary Exception from the yielded body is logged and suppressed before the handle is closed. Logging or close failures can still escape. The internal memory fallback in _FileIO._doWrite belongs to the missing-handle branch; it is not a fallback for every write failure.

The writer discards fd.write's result and returns the handle after context cleanup. That return does not establish that the body completed successfully or that bytes are durable. FileLocalCache.write discards the redirect result altogether. For mod-owned persistence, define an explicit success/failure result when callers need an acknowledgement, and keep opening, writing, closing and durable storage as distinct boundaries.

Targeted static inspection of original Python bytecode from WG 2.4.1.1 #989 confirmed the tuple binding; the readable dump's apparent second context manager is an extraction artifact. This evidence covers the inspected methods, not whole-module parity or a real file-failure, atomicity or durability test.

Do not persist transient identity

Arena IDs, entity IDs, view instances, callback IDs, and dropdown indexes are temporary. Persist stable semantic values and reconstruct presentation state when the relevant system becomes ready.

Preferences-root storage

When a mod needs account-independent local storage, the client preferences path is a useful anchor because it follows the active client profile. Verify the helper in the target build, derive the containing directory, and store only files owned by your mod in a separate subdirectory.

Illustrative Python 2.7 example. The missing _owned_storage_dir() supplies all anchoring and ownership policy. Only its literal None selects None; otherwise the join result is returned. An empty root can yield a relative filename. The shell creates no directory, checks no containment or writability, performs no read/write or recovery, and propagates failures. The helper's name does not establish a safe preferences-root storage location.

def _config_path():
    root = _owned_storage_dir()
    if root is None:
        return None
    return os.path.join(root, 'settings.json')

Schema and migration

from copy import deepcopy

DEFAULTS = {
    'schema': 2,
    'enabled': True,
    'position': {'x': 40, 'y': 40},
}

def _load_config(path):
    defaults = deepcopy(DEFAULTS)
    raw = _read_json(path)
    if not isinstance(raw, dict):
        return defaults
    raw = _migrate_config(raw)
    return _validate_and_merge(raw, defaults)

Copy the JSON-shaped default tree for each load. A top-level dict(DEFAULTS) would still share the nested position dictionary, allowing an edited result to change later fallbacks. Pass the private copy into validation, and require custom validators to return mutable values owned by that result rather than borrowed template subtrees.

Here the default copy is made before _read_json. Copy failure prevents reading; reader failure propagates despite the prepared defaults. Only a returned non-dictionary selects those defaults. An admitted raw dictionary goes to _migrate_config without copying its tree, then the unchecked result and private defaults go to _validate_and_merge.

These helpers are undefined here. The validator's return is forwarded unchanged, without a final type, version or ownership check. Its name does not establish safe merging or validation; helpers may mutate their inputs or retain borrowed state. This shell provides no fallback on exceptions, save, replacement, retry, cleanup, traceback logging or module version. Define those owner contracts before using it as part of a mod. The separate migration-loop illustration likewise omits progress and final-version checks. These exact fragments were reviewed, not executed.

Apply migrations one schema version at a time. Write to a temporary file, flush it, then replace the destination with a defined recovery policy; staging alone does not prove crash durability.

Saved values are not recursively merged defaults

The ordinary account preference getter returns a successfully decoded saved value directly. It does not recursively merge new default keys or validate nested types. Missing/corrupt data falls back to a copied registered default; section-specific migrations can run separately. Own schema migration when adding fields and preserve the distinctions between missing, false, zero and None.

The recursive helper for new-setting counters reconciles novelty indicators, not arbitrary configuration. Follow the caller and storage owner before borrowing a merge-looking function; it is not a general migration or disk-flush API.

Write frequency

Update in-memory state immediately, but debounce disk writes from drag, slider, or rapid event streams. Flush on a bounded delay and on orderly teardown.

Security and privacy

Do not log tokens, secrets, private endpoints, or complete server payloads. Treat downloaded configuration as untrusted input with size, type, and path limits.

Common mistakes

  • Replacing defaults with unvalidated JSON.
  • Persisting UI indexes that change when options reorder.
  • Writing on every mouse-move event.
  • Using a temporary installation directory for data meant to survive updates.
  • Allowing arbitrary paths from configuration or network data.

Verification trail

Confirm the client preferences directory, account identity timing, existing configuration conventions, and shutdown path. Test empty, corrupt, old-schema, read-only, and interrupted-write cases.