Data Protective Transformer
Wrapping data so it can be read but never changed, in JavaScript and Python. The holes in my first versions and how I closed them.
Sometimes I want to hand data to some code and be sure it can read the data but not change it. A config object, an API response passed around the app, user data going into a plugin. The idea is simple: wrap the data once, read it the normal way (data.from_user.name), and get an error on any write, at any depth.
The data I use in the examples below:
const message = {
id: 1,
from_user: { id: 2, name: "Qiyas" },
replies: [{ text: "hi" }],
};
I wrote this once in JavaScript and once in Python. Reading both again, both had holes. Here is what was wrong and what I use now.
JavaScript
The first version wrapped the object in a Proxy and threw on set and deleteProperty. Three problems:
Object.defineProperty(data, "id", { value: 99 })went straight through. A proxy without adefinePropertytrap passes that call to the real object, and the value changed. Same withObject.setPrototypeOf.- Every read created a new proxy, so
data.from_user === data.from_userwasfalse. That breaks memoization,Setlookups and React’s equality checks, and it allocates on every read. - The
prop in target ? ... : undefinedcheck did nothing. A missing property is alreadyundefined.
The fixed version:
const views = new WeakMap();
const refuse = () => {
throw new TypeError("This data is read-only");
};
const handler = {
get(target, key, receiver) {
return readOnly(Reflect.get(target, key, receiver));
},
set: refuse,
deleteProperty: refuse,
defineProperty: refuse,
setPrototypeOf: refuse,
preventExtensions: refuse,
};
function readOnly(value) {
if (typeof value !== "object" || value === null) return value;
let view = views.get(value);
if (!view) {
view = new Proxy(value, handler);
views.set(value, view);
}
return view;
}
Every way of changing an object now has a trap. The WeakMap gives each object exactly one proxy, so the same object always comes back as the same view, and it does not keep the data alive after you drop it. Reflect.get passes the receiver along, so getters on classes still work. Arrays need no special case: data.replies.map(...) works, data.replies.push(...) throws.
What it does not handle:
Map,SetandDatedo not work behind a proxy. Their methods check the real object and throw aTypeError. Convert them to plain objects or arrays first.- Do not wrap an object that is already frozen. Proxy rules say a frozen property must return exactly its own value, and here it would return a proxy, so the read throws.
- This is a read-only view, not a copy. Whoever holds the original object can still change it, and you will see the change through the view.
When I need a real snapshot that nobody can change, I copy and freeze instead:
function deepFreeze(value) {
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
Object.freeze(value);
for (const key of Reflect.ownKeys(value)) deepFreeze(value[key]);
}
return value;
}
const snapshot = deepFreeze(structuredClone(message));
In strict mode, which is every ES module, writing to a frozen object throws. Outside strict mode the write fails silently, which is easy to miss. So: the proxy when I want a cheap live view, the frozen copy when the data must not move.
Python
The first Python version kept the fields in a dict and blocked __setattr__. When I actually tested it, more broke than I expected:
toJSON()useddumpswithout importing it. It raisedNameErrorthe first time anyone called it.- Only dicts were converted. A list inside the data stayed a normal list, so
data.replies.append(...)worked and the dicts inside it could be edited freely. “Read-only at any depth” was not true. - The inner dict was reachable:
data._data["id"] = 99changed the value. copy.copy,copy.deepcopyandpickleall crashed withRecursionError. They create an empty object first, then__getattr__looks for_data, which does not exist yet, so it keeps calling itself.- Keys had to be strings because of
**data.{1: "a"}raisedTypeError.
The fixed version:
import json
from collections.abc import Mapping
from types import MappingProxyType
def freeze(value):
if isinstance(value, Mapping):
return ReadOnly(value)
if isinstance(value, (list, tuple)):
return tuple(freeze(v) for v in value)
if isinstance(value, set):
return frozenset(value)
return value
class ReadOnly:
__slots__ = ("_data",)
def __init__(self, data):
frozen = {key: freeze(value) for key, value in data.items()}
object.__setattr__(self, "_data", MappingProxyType(frozen))
def __getattr__(self, name):
if name == "_data":
raise AttributeError(name)
try:
return self._data[name]
except KeyError:
raise AttributeError(f"no field named {name!r}") from None
def __getitem__(self, key):
return self._data[key]
def __setattr__(self, name, value):
raise AttributeError("read-only")
def __delattr__(self, name):
raise AttributeError("read-only")
def __reduce__(self):
return ReadOnly, (dict(self._data),)
def __repr__(self):
return f"ReadOnly({dict(self._data)!r})"
def to_json(self, **kwargs):
return json.dumps(self, default=lambda o: dict(o._data), **kwargs)
What changed:
freezewalks the whole structure. Dicts becomeReadOnly, lists become tuples, sets become frozensets.MappingProxyTypeis a read-only window onto a dict, so_datacan be read but not written.__getattr__refuses to look up_dataitself, which ends the recursion, and__reduce__tellscopyandpicklehow to rebuild the object.__getitem__makesdata[1]anddata["first-name"]work for keys that are not valid attribute names.to_jsonimportsjsonproperly, and tuples come out as JSON arrays.
Using it:
data = ReadOnly({
"id": 1,
"from_user": {"id": 2, "name": "Qiyas"},
"replies": [{"text": "hi"}],
})
data.from_user.name # 'Qiyas'
data.replies[0].text # 'hi'
data.id = 5 # AttributeError
data.replies.append(1) # AttributeError
Python has no truly private fields, so someone determined can still call object.__setattr__(data, "_data", {...}). This protects against mistakes, not against someone trying to break it on purpose.
If you know the shape of your data in advance, @dataclass(frozen=True) or a Pydantic model with frozen=True gives the same protection plus type checks. ReadOnly is for when the shape comes from JSON and you do not control it.
The full code is on GitHub Gist: JavaScript and Python. The first versions are still in the revision history.
-
Python List Optimization: Efficient Management of Duplicate Elements
2026-06-21 · 3 min