DotDict
DotDict extends Python’s dict with attribute lookup, assignment and deletion. Use it for small dynamic records when dictionary semantics are still useful. It preserves the supplied values; it does not validate data or coerce scalar types.
Import and direct use
Section titled “Import and direct use”from orionis.support.types import DotDict
settings = DotDict({"server": {"port": 8000}})assert settings.server.port == 8000settings.server.port = 9000assert settings["server"]["port"] == 9000Dependency injection
Section titled “Dependency injection”The class has no framework service dependency. Direct construction is sufficient. Orionis can also inject an empty DotDict into a class constructor; populate it explicitly for the operation that needs it. Automatic construction does not load request data or application settings.
import asynciofrom orionis.container.container import Containerfrom orionis.support.types import DotDict
class Report: def __init__(self, values: DotDict) -> None: self.values = values
async def main() -> None: class LocalContainer(Container): pass
report = await LocalContainer().make(Report) report.values.name = "Orionis" assert report.values["name"] == "Orionis"
asyncio.run(main())Construction: inherited dict constructor
Section titled “Construction: inherited dict constructor”DotDict(), DotDict(mapping, /, **kwargs) and DotDict(iterable, /, **kwargs) create a mutable dictionary. Keyword values overwrite duplicate source keys. Construction does not eagerly wrap nested dictionaries or dictionaries inside lists. Invalid input follows dict: for example, malformed key/value pairs raise ValueError and unhashable keys raise TypeError.
from orionis.support.types import DotDict
data = DotDict([("name", "old")], name="Orionis", config={"debug": True})assert data["name"] == "Orionis"assert type(data["config"]) is dictassert DotDict() == {}__getattr__(self, key: str) -> object | None
Section titled “__getattr__(self, key: str) -> object | None”Attribute lookup uses this fallback when normal Python attribute lookup finds nothing. It returns None for an absent key. An exact plain dict value becomes a DotDict, and that replacement is stored in the parent. Other values, including dictionary subclasses and dictionaries inside lists, remain unchanged. Existing method names such as items take precedence over keys; use brackets for those keys. Because missing attributes return None, hasattr(data, "missing") is True; test key membership instead.
from orionis.support.types import DotDict
data = DotDict(config={"debug": True}, items="stored value")assert data.config.debug is Trueassert data["config"] is data.configassert data.missing is Noneassert hasattr(data, "missing") is Trueassert "missing" not in dataassert callable(data.items)assert data["items"] == "stored value"__setattr__(self, key: str, value: object) -> None
Section titled “__setattr__(self, key: str, value: object) -> None”data.name = value stores a dictionary entry and returns None through the method. An exact dict is wrapped once; nested wrapping happens during later attribute access. The operation replaces an existing entry. It does not create an independent instance attribute.
from orionis.support.types import DotDict
data = DotDict()data.config = {"nested": {"enabled": True}}assert type(data["config"]) is DotDictassert type(data["config"]["nested"]) is dictdata.name = "Orionis"data.name = "Updated"assert data["name"] == "Updated"__delattr__(self, key: str) -> None
Section titled “__delattr__(self, key: str) -> None”del data.name deletes the matching entry in place. It returns None through the method and raises AttributeError when the key is absent, chaining the original KeyError.
from orionis.support.types import DotDict
data = DotDict(name="Orionis")del data.nameassert "name" not in datatry: del data.nameexcept AttributeError as error: assert isinstance(error.__cause__, KeyError)get(self, key: str, default: object | None = None) -> object | None
Section titled “get(self, key: str, default: object | None = None) -> object | None”Returns the stored value or default (None by default). An exact dict result is wrapped and stored under key. This also applies to a dictionary supplied as the default: reading an absent key with that default inserts the wrapped default. Other defaults do not insert an entry. Values are not coerced.
from orionis.support.types import DotDict
data = DotDict(config={"debug": True})assert data.get("config").debug is Trueassert data.get("absent") is Noneassert "absent" not in datafallback = data.get("created", {"port": 8000})assert isinstance(fallback, DotDict)assert data["created"] is fallbackassert fallback.port == 8000export(self) -> dict[str, Any]
Section titled “export(self) -> dict[str, Any]”Returns a new plain dictionary, recursively converting dictionary values to plain dictionaries. It does not mutate the source. Traversal follows dictionary values only: lists, tuples, custom objects and their contents keep their references. Dictionary subclasses are normalized. There is no cycle cache; a cycle reached through dictionary values raises RecursionError.
from orionis.support.types import DotDict
rows = [DotDict(id=1)]data = DotDict(config=DotDict(debug=True), rows=rows)result = data.export()assert type(result) is dictassert type(result["config"]) is dictassert result["rows"] is rowsassert type(result["rows"][0]) is DotDictresult["config"]["debug"] = Falseassert data.config.debug is Truecopy(self) -> DotDict
Section titled “copy(self) -> DotDict”Returns a new DotDict, recursively copying dictionary values into new DotDict objects. Despite the source docstring’s term “deep copy”, this method shares non-dictionary values, including mutable lists. It neither mutates the source nor preserves shared dictionary identity. Dictionary cycles raise RecursionError. Use Python’s copy.deepcopy when lists and other supported values must also be copied.
from orionis.support.types import DotDict
data = DotDict(config={"debug": True}, rows=[1])result = data.copy()assert type(result) is DotDictassert type(result["config"]) is DotDictresult.config.debug = Falseassert data.config.debug is Trueresult.rows.append(2)assert data.rows == [1, 2]Inherited clear(self) -> None
Section titled “Inherited clear(self) -> None”Removes every entry in place and returns None.
from orionis.support.types import DotDict
data = DotDict(a=1)assert data.clear() is Noneassert data == {}Inherited fromkeys(iterable, value=None, /)
Section titled “Inherited fromkeys(iterable, value=None, /)”The inherited class method returns a new DotDict, assigning the same value reference to every key. Its default is None; it does not wrap dictionary values. Unhashable keys raise TypeError.
from orionis.support.types import DotDict
shared = []data = DotDict.fromkeys(["first", "second"], shared)assert type(data) is DotDictdata["first"].append(1)assert data["second"] == [1]assert DotDict.fromkeys(["unset"])["unset"] is NoneInherited items(self)
Section titled “Inherited items(self)”Returns a dynamic dictionary view of (key, value) pairs without wrapping nested values. It does not mutate the dictionary. Changing its size during iteration may raise RuntimeError.
from orionis.support.types import DotDict
data = DotDict(a=1)view = data.items()data["b"] = 2assert list(view) == [("a", 1), ("b", 2)]Inherited keys(self)
Section titled “Inherited keys(self)”Returns a dynamic view of keys in insertion order without mutation. Iterating while the dictionary changes size may raise RuntimeError.
from orionis.support.types import DotDict
data = DotDict(a=1)view = data.keys()data["b"] = 2assert list(view) == ["a", "b"]Inherited values(self)
Section titled “Inherited values(self)”Returns a dynamic view of the stored values without wrapping dictionaries or mutating the source. Iterating while its size changes may raise RuntimeError.
from orionis.support.types import DotDict
data = DotDict(config={"debug": True})view = data.values()assert type(next(iter(view))) is dictdata["count"] = 2assert len(view) == 2Inherited pop(self, key, /, [default])
Section titled “Inherited pop(self, key, /, [default])”Removes and returns an entry without converting its value. For an absent key it returns the supplied default; if no default was supplied it raises KeyError. There is no implicit None default.
from orionis.support.types import DotDict
data = DotDict(config={"debug": True})assert type(data.pop("config")) is dictassert data.pop("missing", None) is Nonetry: data.pop("missing")except KeyError: assert data == {}Inherited popitem(self)
Section titled “Inherited popitem(self)”Removes and returns the most recently inserted (key, value) pair. It raises KeyError when empty and does not wrap the returned value.
from orionis.support.types import DotDict
data = DotDict(a=1, b=2)assert data.popitem() == ("b", 2)assert data == {"a": 1}Inherited setdefault(self, key, default=None, /)
Section titled “Inherited setdefault(self, key, default=None, /)”Returns an existing value, or inserts and returns default when absent. Neither path wraps dictionary values. The default is None. Unhashable keys raise TypeError.
from orionis.support.types import DotDict
data = DotDict()fallback = {"debug": True}assert data.setdefault("config", fallback) is fallbackassert type(data["config"]) is dictassert data.setdefault("config", {"debug": False}) is fallbackassert data.setdefault("unset") is NoneInherited update(self, [mapping_or_iterable], /, **kwargs) -> None
Section titled “Inherited update(self, [mapping_or_iterable], /, **kwargs) -> None”Adds or replaces entries in place and returns None. Keyword arguments take precedence over the positional source. It does not wrap dictionaries. Invalid inputs can raise TypeError or ValueError; entries processed before an error may already be stored.
from orionis.support.types import DotDict
data = DotDict(a=1)assert data.update({"a": 2, "config": {"debug": True}}, a=3) is Noneassert data["a"] == 3assert type(data["config"]) is dictDictionary protocols
Section titled “Dictionary protocols”These operations are inherited from dict (and its object protocols), without DotDict-specific value conversion. The following table gives their Python entry points. Dictionary size changes during iteration can raise RuntimeError; item operations require hashable keys.
from orionis.support.types import DotDict
data = DotDict(a=1, b=2)data["config"] = {"debug": True}assert type(data["config"]) is dictassert data["a"] == 1del data["config"]assert list(data) == ["a", "b"]assert list(reversed(data)) == ["b", "a"]assert len(data) == 2 and bool(data)assert "a" in dataassert data == {"a": 1, "b": 2}assert data != {"a": 9}merged = data | {"b": 3}assert type(merged) is dict and merged["b"] == 3reverse_merged = {"b": 0} | dataassert type(reverse_merged) is dict and reverse_merged["b"] == 2original = datadata |= [("c", 3)]assert data is original and data.c == 3assert repr(data) == "{'a': 1, 'b': 2, 'c': 3}"assert str(data) == repr(data)try: data["missing"]except KeyError: pass__hash__ = None
Section titled “__hash__ = None”DotDict is mutable and unhashable. Calling hash(data) or using an instance as a dictionary key raises TypeError.
from orionis.support.types import DotDict
try: hash(DotDict(a=1))except TypeError: passPython copy and serialization
Section titled “Python copy and serialization”No __copy__, __deepcopy__ or custom serialization hook is defined. Python’s standard copy.copy uses the inherited reduction protocol and makes a shallow DotDict; copy.deepcopy recursively copies supported contents. Pickle round-trips preserve the DotDict type for ordinary serializable values. JSON accepts it as a dictionary and decoding returns a plain dict; JSON’s normal restrictions on keys, values and cycles apply. export() is not JSON encoding.
import copyimport jsonimport picklefrom orionis.support.types import DotDict
data = DotDict(rows=[1])shallow = copy.copy(data)deep = copy.deepcopy(data)restored = pickle.loads(pickle.dumps(data))assert type(shallow) is DotDict and shallow["rows"] is data["rows"]assert type(deep) is DotDict and deep["rows"] is not data["rows"]assert type(restored) is DotDict and restored == dataassert json.loads(json.dumps(data)) == {"rows": [1]}