# mewwts/addict: a Python dict subclass with attribute access and recursive defaults

> addict turns nested dictionaries into objects you can build with dotted attribute assignment, while keeping dict semantics, JSON serialization and a to_dict() escape hatch. It is a small convenience library for people who write deep nested structures by hand.

**mewwts/addict** — The Python Dict that's better than heroin.

- Repository: https://github.com/mewwts/addict
- Stars: 2,548 · Forks: 134
- Language: Python
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/mewwts-addict

## The problem addict solves for hand-written nested dictionaries

Plain Python dictionaries require you to spell out every level of a nested structure, and a missing intermediate level raises KeyError before you can assign anything into it. The README opens with exactly this pain, showing a multi-line Elasticsearch query body built from literal dicts, then rewriting it as three lines of attribute assignment. The project states that it "rose from the entirely tiresome creation of Elasticsearch queries in Python", which tells you the intended audience: developers who construct query DSLs, config trees or aggregation results by hand and are tired of writing braces.

The library is not a general data-modelling tool. It does not add schema validation, type coercion or serialization beyond what dict already provides. What it adds is syntax: attribute get and set on top of item get and set, plus a __missing__ that returns an empty Dict instead of raising. That single behaviour is what makes chained assignment like mapping.a.b.c.d.e = 2 work, because each intermediate lookup creates a Dict on demand and returns it.

## How Dict works: subclassing dict, __missing__ and recursive update

Dict inherits from dict. Attribute access is implemented so that reading an attribute resolves to an item lookup, and writing an attribute resolves to an item assignment. Because the default for a missing key is an empty Dict rather than a KeyError, a chain of attribute reads walks down through newly created levels until it reaches the level you are assigning into. The README demonstrates this with mapping.a.b.c.d.e = 2 producing {'a': {'b': {'c': {'d': {'e': 2}}}}}.

The constructor behaves differently from later assignment. If you pass an iterable, addict iterates through it, clones the values and converts nested dicts into Dicts, so the original mapping and the new Dict no longer share references: mapping['a'] is dictionary['a'] returns False. That cloning is limited to construction. Setting a value afterwards, whether by attribute or by item syntax, stores the reference untouched, and the README shows a.b is b returning True.

update() is also changed. A normal dict update replaces a nested value wholesale, so {'a': {'b': 3}}.update({'a': {'c': 4}}) leaves only c. Dict recurses instead, and the README shows the result as {'a': {'b': 3, 'c': 4}}. That recursion is convenient for merging configuration fragments and dangerous for anyone who expects dict's replace semantics.

Keys that are not strings cannot be reached with attribute syntax, since int is not a valid attribute name. The README handles this by mixing both forms, using addicted[2] = [1, 2, 3] alongside addicted.a.b['c'].d.e. Attribute names that collide with dict methods are also protected: mapping.keys = 2 raises AttributeError with the message "'Dict' object attribute 'keys' is read-only", while a['keys'] = 2 works normally. The README notes there are no restrictions on keys beyond what a regular dict imposes.

## Installing addict and building your first nested Dict

The README gives two installation paths. The pip route is the one most users will take, and it drops a single package with no dependencies beyond the standard library.

```bash
pip install addict
```

On conda, the package is available from conda-forge:

```bash
conda install addict -c conda-forge
```

After installation, import Dict and assign through attributes. The following snippet mirrors the README's first example: three lines replace a nested literal, and printing the object shows the full structure.

```python
from addict import Dict

body = Dict()
body.query.filtered.query.match.description = 'addictive'
body.query.filtered.filter.term.created_by = 'Mats'
print(body)
```

You should see a nested dict with a, query, filtered, query, match and description levels. Note that no intermediate key was ever created explicitly; each attribute read created an empty Dict and returned it.

The counting example in the README is the clearest demonstration that the default-missing behaviour is the point rather than a side effect. It builds a multi-level counter with counter[born][gender][eyes] += 1, and the first read of each missing level creates the Dict that the += then increments. The README positions this as an advantage over collections.Counter because it counts along more than one dimension at once.

When you need to hand the result to code that expects a plain dict, call to_dict(). The README's pattern is to build the structure with addict, then pass body.to_dict() into a third-party function. The returned object is a regular dict clone, and attribute access on it fails with AttributeError: 'dict' object has no attribute 'a', which is the intended signal that you are back in normal dict territory.

## The default-missing behaviour is the real trade-off

Returning an empty Dict for any missing key means a typo in a key name does not raise. Reading config.databse.host creates a databse level and returns an empty Dict, and the failure surfaces later as an empty value rather than an exception at the point of the mistake. The README acknowledges this directly and offers an override: subclass Dict and define __missing__ to raise KeyError. It also warns that doing so costs you the shorthand assignment, because the chain can no longer create intermediate levels. You cannot have both strict missing-key errors and dotted assignment in the same class.

The constructor's cloning is a second source of surprise. Because Dict(mapping) deep-copies nested dicts into Dicts, code that mutates the original mapping after construction will not see those changes reflected in the Dict, and vice versa. The README states this explicitly with the mapping['a'] is dictionary['a'] comparison. Anyone who treats Dict as a thin view over an existing structure will be wrong about identity.

A third constraint is name collision with dict itself. Attribute assignment is blocked for any name that dict already defines, so keys, items, values, update and similar names must be set with item syntax. That is a documented boundary rather than a bug, but it means attribute syntax is not a universal replacement for brackets, and code that mixes both styles needs a reason for each choice.

Finally, the classifiers in setup.py list Python 2.7 and 3.6 through 3.9, and the README says every build is tested towards 2.7, 3.6 and 3.7. Nothing in the repository states support for newer interpreters, so verify on your target version rather than assuming.

## addict compared with types.SimpleNamespace and collections.defaultdict

types.SimpleNamespace gives attribute access over a namespace object, but it is not a dict subclass, it does not support item syntax, and it does not serialize to JSON without conversion. addict keeps dict underneath, so json.dumps works on it directly, which the README calls out under Perks. If your data has to cross a JSON boundary, SimpleNamespace is the wrong shape.

collections.defaultdict solves the missing-key problem but not the nesting problem. A defaultdict of defaultdict requires you to declare the factory at each level, and it still needs bracket syntax, so counter[born][gender][eyes] += 1 becomes a chain of brackets that you must construct correctly. addict's recursive default is what lets the same expression work without pre-declaring factories. The README makes this comparison itself, arguing that Dict allows counting by multiple levels where Counter does not.

A plain dict with dict.setdefault calls can replicate the intermediate-creation behaviour, at the cost of writing setdefault at every level. addict trades that verbosity for a class that behaves differently from dict in three places: missing keys, construction-time cloning, and update recursion. Whether that trade is worth it depends on how much of your code is nested-literal construction versus how much is code that assumes standard dict semantics.

## Maintenance, releases and what the MIT licence means here

The repository is not archived, and the last push was on 2026-03-24. The most recent tagged release is v2.4.0 from 2020-11-21, with v2.3.0 adding support for | and |= before it. That gap between branch activity and tagged releases is worth noting: fixes may exist on master that are not in the version you install from PyPI, so check whether a behaviour you need is present in the released package or only in the repository.

The licence is MIT, which permits commercial and closed-source use, modification and redistribution provided the copyright notice and permission notice are retained. setup.py includes the LICENSE in package_data, so the licence ships with the installed package. This is a summary of what the repository states, not legal advice; if your organisation has licence review requirements, route the LICENSE file through that process.

Upgrade cost is low in normal use. The public surface is a single class with attribute and item access, a to_dict() method, and an altered update(). Version 2.3.0 added the union operators, which means code using | on Dict objects requires at least that release. The README also mentions Python 2 support, which places a floor on how far back the library intends to be compatible and, conversely, means the codebase carries compatibility scaffolding that newer projects do not need.

## Conclusion

Adopt addict if your code builds deeply nested dictionaries by hand, especially Elasticsearch-style query bodies, and you want dotted assignment plus automatic intermediate dicts. Do not adopt it if you need strict KeyError behaviour on missing keys, if your keys are not strings and you expect attribute syntax to work, or if you are shipping the object across a module boundary without calling to_dict(). Before committing, verify two things: that the default behaviour of returning an empty Dict for a missing key does not silently hide a typo in your key names, and that your Python version is covered by the classifiers in setup.py, which list 2.7 and 3.6 through 3.9. The last push to the repository was on 2026-03-24, while the most recent tagged release, v2.4.0, dates from 2020-11-21, so treat the release channel as the stable surface and the branch as the place where fixes land.

## FAQ

### How do I install addict in Python?

The README gives two options: pip install addict, or conda install addict -c conda-forge. Both install a single package that inherits from dict, so no other dependencies are introduced.

### How do I convert an addict Dict back to a normal dict?

Call the to_dict() method, which returns a regular dict clone of the addict dictionary. The README recommends this when shipping the object to another module, since attribute access on the result raises AttributeError as it would on any plain dict.

### Why does addict return an empty Dict instead of raising KeyError?

Missing keys behave like defaultdict(Dict), so an absent key returns an empty Dict rather than raising. That is what allows chained assignment such as mapping.a.b.c.d.e = 2, and the README notes you can override __missing__ to raise KeyError, at the cost of losing the shorthand assignment.

### Can I set a key called keys or items on an addict Dict?

Not with attribute syntax. addict refuses to override attributes native to dict, so mapping.keys = 2 raises AttributeError with the message that the attribute is read-only. Item syntax works: a['keys'] = 2 stores the value normally.

### Does addict's update() replace nested dicts like a normal dict?

No. A normal dict update overwrites the nested value, while addict recurses and merges, so updating {'a': {'b': 3}} with {'a': {'c': 4}} leaves both b and c in place. The README presents this as a deliberate convenience change.

## Sources

- [Issues](https://github.com/mewwts/addict/issues)
- [License: MIT](https://github.com/mewwts/addict/blob/master/LICENSE)
- [mewwts/addict on GitHub](https://github.com/mewwts/addict)
- [README](https://github.com/mewwts/addict/blob/master/README.md)
- [Releases](https://github.com/mewwts/addict/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mewwts-addict
