dotted-notation 0.32.2
Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
License: MIT
Requires Python: >=3.6

# Dotted

Sometimes you want to fetch data from a deeply nested data structure. Dotted notation
helps you do that.

## Installation

    pip install dotted-notation

Since this package includes the [**`dq`** command-line tool](#cli-dq), several data formats are supported:

| Format | Status   |
|--------|----------|
| JSON   | included |
| JSONL  | included |
| CSV    | included |
| YAML   | optional |
| TOML   | optional |

To install optional format support:

    pip install dotted-notation[all]

Or pick only what you need:

    pip install dotted-notation[yaml,toml]

## Table of Contents

- [Safe Traversal (Optional Chaining)](#safe-traversal-optional-chaining)
- [Why Dotted?](#why-dotted)
- [Breaking Changes](#breaking-changes)
- [API](#api)
  - [Get](#get)
  - [Update](#update)
  - [Remove](#remove)
  - [Match](#match)
  - [Expand](#expand)
  - [Overlaps](#overlaps)
  - [Has](#has)
  - [Mutable](#mutable)
  - [Setdefault](#setdefault)
  - [Pluck](#pluck)
  - [Unpack](#unpack)
  - [Build](#build)
  - [Apply](#apply)
  - [Assemble](#assemble)
  - [Quote](#quote)
  - [Normalize](#normalize)
  - [AUTO](#auto)
  - [Multi Operations](#multi-operations)
- [Paths](#paths)
  - [Key fields](#key-fields)
  - [Bracketed fields](#bracketed-fields)
  - [Attr fields](#attr-fields)
  - [Slicing](#slicing)
  - [Dot notation for sequence indexing](#dot-notation-for-sequence-indexing)
  - [Empty path (root access)](#empty-path-root-access)
- [Typing & Quoting](#typing--quoting)
  - [Numeric types](#numeric-types)
  - [Quoting](#quoting)
  - [The numericize `#` operator](#the-numericize--operator)
  - [Container types](#container-types)
  - [Bytes literals](#bytes-literals)
- [Patterns](#patterns)
  - [Wildcards](#wildcards)
  - [Regular expressions](#regular-expressions)
  - [The match-first operator](#the-match-first-operator)
  - [Slicing vs Patterns](#slicing-vs-patterns)
- [Recursive Traversal](#recursive-traversal)
  - [The recursive operator `*`](#the-recursive-operator-)
  - [Recursive wildcard `**`](#recursive-wildcard-)
  - [Depth slicing](#depth-slicing)
  - [Recursive with value guard](#recursive-with-value-guard)
  - [Recursive update and remove](#recursive-update-and-remove)
  - [Recursive match](#recursive-match)
- [Grouping](#grouping)
  - [Path grouping](#path-grouping)
  - [Operation grouping](#operation-grouping)
- [Operators](#operators)
  - [The append `+` operator](#the-append--operator)
  - [The append-unique `+?` operator](#the-append-unique--operator)
  - [The invert `-` operator](#the-invert---operator)
  - [The NOP `~` operator](#the-nop--operator)
  - [The cut `#` operator](#the-cut--operator)
  - [The soft cut `##` operator](#the-soft-cut--operator)
  - [The numericize `#` operator](#the-numericize--operator-1)
- [Filters](#filters)
  - [The key-value filter](#the-key-value-filter)
  - [The key-value first filter](#the-key-value-first-filter)
  - [Conjunction vs disjunction](#conjunction-vs-disjunction)
  - [Grouping with parentheses](#grouping-with-parentheses)
  - [Filter negation and not-equals](#filter-negation-and-not-equals)
  - [Boolean and None filter values](#boolean-and-none-filter-values)
  - [Value guard](#value-guard)
  - [Container filter values](#container-filter-values)
  - [Type prefixes](#type-prefixes)
  - [String glob patterns](#string-glob-patterns)
  - [Bytes glob patterns](#bytes-glob-patterns)
  - [Value groups](#value-groups)
  - [Dotted filter keys](#dotted-filter-keys)
  - [Slice notation in filter keys](#slice-notation-in-filter-keys)
- [Transforms](#transforms)
  - [Built-in Transforms](#built-in-transforms)
  - [Container transform arguments](#container-transform-arguments)
  - [Custom Transforms](#custom-transforms)
- [Constants and Exceptions](#constants-and-exceptions)
- [CLI (`dq`)](#cli-dq)
  - [File input](#file-input)
  - [Format conversion](#format-conversion)
  - [Path files](#path-files)
  - [Projection](#projection)
  - [Unpack](#unpack)
  - [Pack](#pack)
- [FAQ](#faq)
  - [Why do I get a tuple for my get?](#why-do-i-get-a-tuple-for-my-get)
  - [How do I craft an efficient path?](#how-do-i-craft-an-efficient-path)
  - [Why do I get a RuntimeError when updating with a slice filter?](#why-do-i-get-a-runtimeerror-when-updating-with-a-slice-filter)

<a id="safe-traversal-optional-chaining"></a>
## Safe Traversal (Optional Chaining)

Like JavaScript's optional chaining operator (`?.`), dotted safely handles missing paths.
If any part of the path doesn't exist, `get` returns `None` (or a specified default)
instead of raising an exception:

    >>> import dotted
    >>> d = {'a': {'b': 1}}
    >>> dotted.get(d, 'a.b.c.d.e')  # path doesn't exist
    None
    >>> dotted.get(d, 'a.b.c.d.e', 'default')  # with default
    'default'
    >>> dotted.get(d, 'x.y.z', 42)  # missing from the start
    42

This makes dotted ideal for safely navigating deeply nested or uncertain data structures
without defensive coding or try/except blocks.

<a id="why-dotted"></a>
## Why Dotted?

Several Python libraries handle nested data access. Here's how dotted compares:

| Feature | dotted | glom | jmespath | pydash |
|---------|--------|------|----------|--------|
| Safe traversal (no exceptions) | ✅ | ✅ | ✅ | ✅ |
| Familiar dot notation | ✅ | ❌ (custom spec) | ❌ (JSON syntax) | ✅ |
| Pattern matching (wildcards) | ✅ | ✅ | ❌ | ❌ |
| Regex patterns | ✅ | ❌ | ❌ | ❌ |
| In-place mutation | ✅ | ✅ | ❌ (read-only) | ✅ |
| Attribute access (`@attr`) | ✅ | ✅ | ❌ | ✅ |
| Transforms/coercion | ✅ | ✅ | ✅ | ❌ |
| Slicing | ✅ | ❌ | ✅ | ❌ |
| Filters | ✅ | ❌ | ✅ | ❌ |
| AND/OR/NOT filters | ✅ | ❌ | ✅ | ❌ |
| Grouping `(a,b)`, `(.a,.b)` | ✅ | ❌ | ❌ | ❌ |
| Recursive traversal (`**`, `*key`) | ✅ | ✅ | ❌ | ❌ |
| Depth slicing (`**:-1`, `**:2`) | ✅ | ❌ | ❌ | ❌ |
| NOP (~) match but don't update | ✅ | ❌ | ❌ | ❌ |
| Cut (#) and soft cut (##) in disjunction | ✅ | ❌ | ❌ | ❌ |
| Container filter values (`[1, ...]`, `{k: v}`) | ✅ | ❌ | ❌ | ❌ |
| String/bytes glob patterns (`"pre"..."suf"`) | ✅ | ❌ | ❌ | ❌ |
| Value groups (`(val1, val2)`) | ✅ | ❌ | ❌ | ❌ |
| Bytes literal support (`b"..."`) | ✅ | ❌ | ❌ | ❌ |
| Zero dependencies | ❌ (pyparsing) | ❌ | ✅ | ❌ |

**Choose dotted if you want:**
- Intuitive `a.b[0].c` syntax that looks like Python
- Pattern matching with wildcards (`*`) and regex (`/pattern/`)
- Both read and write operations on nested structures
- Transforms to coerce types inline (`path|int`, `path|str:fmt`)
- Recursive traversal with `**` (any depth) and `*key` (chain-following), with Python-style depth slicing
- Path grouping `(a,b).c` and operation grouping `prefix(.a,.b)` for multi-access
- **Cut (`#`) in disjunction**—first matching branch wins; e.g. `(a#, b)` or `emails[(*&email="x"#, +)]` for "update if exists, else append"
- **Soft cut (`##`) in disjunction**—suppress later branches only for overlapping paths; e.g. `(**:-2(.*, [])##, *)` for "recurse into containers, fall back to `*` for the rest"
- NOP (`~`) to match without updating—e.g. `(name.~first#, name.first)` for conditional updates
- **String/bytes glob patterns**—match by prefix, suffix, or substring: `*="user_"...`, `*=b"header"...b"footer"`
- **Value groups**—disjunction over filter values: `*=(1, 2, 3)`, `[*&status=("active", "pending")]`

<a id="breaking-changes"></a>
## Breaking Changes

### v0.31.0
- **`{` and `}` are now reserved characters**: Curly braces are used for container
  filter values (dict and set patterns). If you have keys containing literal `{` or `}`,
  quote them: `"my{key}"`.

### v0.30.0
- **`update_if` pred now gates on incoming value, not current value**: Previously,
  `update_if(obj, key, val)` checked `pred(current_value_at_path)`. Now it checks
  `pred(val)`. Default pred changed from `lambda val: val is None` to
  `lambda val: val is not None` — meaning None values are skipped.
  Use path expressions with NOP (`~`) and cut (`#`) for conditional updates based on
  existing values.
- **`remove_if` pred now gates on key, not current value**: Previously,
  `remove_if(obj, key)` checked `pred(current_value_at_path)`. Now it checks
  `pred(key)`. Default pred is `lambda key: key is not None` — meaning None keys
  are skipped.
- **`update_if_multi` / `remove_if_multi`**: Same pred changes as their single
  counterparts.

### v0.28.0
- **`[*=value]` on primitive lists no longer works** — use `[*]=value` (value guard) instead.
  `[*=value]` is a SliceFilter that tests *keys* of dict-like items; primitives have no keys,
  so it now correctly returns `[]`.
- **`[!*=value]` on primitive lists no longer works** — use `[*]!=value` instead.
- **`*&*=value` no longer matches primitives** — use `*=value` (value guard) instead.
- Existing `[*=value]` on dicts/objects is unchanged.
- Existing `&` filter behavior on dict-like nodes is unchanged.

### v0.13.0
- **Filter conjunction operator changed from `.` to `&`**: The conjunction operator for
  chaining multiple filters has changed. Previously, `*.id=1.name="alice"` was used for
  conjunctive (AND) filtering. Now use `*&id=1&name="alice"`. This change enables support
  for dotted paths within filter keys (e.g., `items[user.id=1]` to filter on nested fields).

<a id="api"></a>
## API

Probably the easiest thing to do is pydoc the api layer.

    $ pydoc dotted

Parsed dotted paths are LRU-cached (after the first parse of a given path string), so repeated use of the same path string is cheap.

<a id="get"></a>
### Get

See the Paths, Patterns, and Operators sections below for the full notation.

    >>> import dotted
    >>> dotted.get({'a': {'b': {'c': {'d': 'nested'}}}}, 'a.b.c.d')
    'nested'

<a id="update"></a>
### Update

Update will mutate the object if it can.  It always returns the changed object though. If
it's not mutable, then get via the return.

    >>> import dotted
    >>> l = []
    >>> t = ()
    >>> dotted.update(l, '[0]', 'hello')
    ['hello']
    >>> l
    ['hello']
    >>> dotted.update(t, '[0]', 'hello')
    ('hello',)
    >>> t
    ()

#### Update via pattern

You can update all fields that match pattern given by either a wildcard OR regex.

    >>> import dotted
    >>> d = {'a': 'hello', 'b': 'bye'}
    >>> dotted.update(d, '*', 'me')
    {'a': 'me', 'b': 'me'}

#### Immutable updates

Use `mutable=False` to prevent mutation of the original object:

    >>> import dotted
    >>> data = {'a': 1, 'b': 2}
    >>> result = dotted.update(data, 'a', 99, mutable=False)
    >>> data
    {'a': 1, 'b': 2}
    >>> result
    {'a': 99, 'b': 2}

This works for `remove` as well:

    >>> data = {'a': 1, 'b': 2}
    >>> result = dotted.remove(data, 'a', mutable=False)
    >>> data
    {'a': 1, 'b': 2}
    >>> result
    {'b': 2}

When `mutable=False` is specified and the root object is mutable, `copy.deepcopy()`
is called first. This ensures no mutation occurs even when updating through nested
immutable containers (e.g., a tuple inside a dict).

#### Update if

`update_if` updates only when `pred(val)` is true. Default pred is
`lambda val: val is not None`, so None values are skipped. Use `pred=None`
for unconditional update (same as `update`):

    >>> import dotted
    >>> dotted.update_if({}, 'a', 1)
    {'a': 1}
    >>> dotted.update_if({}, 'a', None)
    {}
    >>> dotted.update_if({}, 'a', '', pred=bool)
    {}

Use `update_if_multi` for batch updates with per-item `(key, val)` or `(key, val, pred)`.

#### Update with NOP (~)

The NOP operator `~` means "match but don't update." Use it when some matches should
be left unchanged. Combine with cut (`#`) for conditional updates:

    >>> import dotted
    >>> data = {'name': {'first': 'hello'}}
    >>> dotted.update(data, '(name.~first#, name.first)', 'world')  # first exists, NOP + cut
    {'name': {'first': 'hello'}}
    >>> data = {'name': {}}
    >>> dotted.update(data, '(name.~first#, name.first)', 'world')  # first missing, update
    {'name': {'first': 'world'}}

<a id="remove"></a>
### Remove

You can remove a field or do so only if it matches value.  For example,

    >>> import dotted
    >>> d = {'a': 'hello', 'b': 'bye'}
    >>> dotted.remove(d, 'b')
    {'a': 'hello'}
    >>> dotted.remove(d, 'a', 'bye')
    {'a': 'hello'}

#### Remove via pattern

Similar to update, all patterns that match will be removed.  If you provide a value as
well, only the matched patterns that also match the value will be removed.

#### Remove if

`remove_if` removes only when `pred(key)` is true. Default pred is
`lambda key: key is not None`, so None keys are skipped. Use `pred=None`
for unconditional remove (same as `remove`):

    >>> import dotted
    >>> dotted.remove_if({'a': 1, 'b': 2}, 'a')
    {'b': 2}
    >>> dotted.remove_if({'a': 1}, None)
    {'a': 1}

Use `remove_if_multi` for batch removal with per-item `(key, val, pred)`.

<a id="match"></a>
### Match

Use to match a dotted-style pattern to a field.  Partial matching is on by default.  You
can match via wildcard OR via regex.  Here's a regex example:

    >>> import dotted
    >>> dotted.match('/a.+/', 'abced.b')
    'abced.b'
    >>> dotted.match('/a.+/', 'abced.b', partial=False)

With the `groups=True` parameter, you'll see how it was matched:

    >>> import dotted
    >>> dotted.match('hello.*', 'hello.there.bye', groups=True)
    ('hello.there.bye', ('hello', 'there.bye'))

In the above example, `hello` matched to `hello` and `*` matched to `there.bye` (partial
matching is enabled by default).

<a id="expand"></a>
### Expand

You may wish to _expand_ all fields that match a pattern in an object.

    >>> import dotted
    >>> d = {'hello': {'there': [1, 2, 3]}, 'bye': 7}
    >>> dotted.expand(d, '*')
    ('hello', 'bye')
    >>> dotted.expand(d, '*.*')
    ('hello.there',)
    >>> dotted.expand(d, '*.*[*]')
    ('hello.there[0]', 'hello.there[1]', 'hello.there[2]')
    >>> dotted.expand(d, '*.*[1:]')
    ('hello.there[1:]',)

<a id="overlaps"></a>
### Overlaps

Test whether two dotted paths overlap—i.e. one is a prefix of the other, or they
are identical. Used internally by [soft cut (`##`)](#the-soft-cut--operator) to
decide which later-branch results to suppress.

    >>> import dotted
    >>> dotted.overlaps('a', 'a.b.c')
    True
    >>> dotted.overlaps('a.b.c', 'a')
    True
    >>> dotted.overlaps('a.b', 'a.b')
    True
    >>> dotted.overlaps('a.b', 'a.c')
    False

<a id="has"></a>
### Has

Check if a key or pattern exists in an object.

    >>> import dotted
    >>> d = {'a': {'b': 1}}
    >>> dotted.has(d, 'a.b')
    True
    >>> dotted.has(d, 'a.c')
    False
    >>> dotted.has(d, 'a.*')
    True

<a id="mutable"></a>
### Mutable

Check if `update(obj, key, val)` would mutate `obj` in place. Returns `False` for
empty paths (root replacement) or when the object or any container in the path
is immutable.

    >>> import dotted
    >>> dotted.mutable({'a': 1}, 'a')
    True
    >>> dotted.mutable({'a': 1}, '')           # empty path
    False
    >>> dotted.mutable((1, 2), '[0]')          # tuple is immutable
    False
    >>> dotted.mutable({'a': (1, 2)}, 'a[0]')  # nested tuple
    False

This is useful when you need to know whether to use the return value:

    >>> data = {'a': 1}
    >>> if dotted.mutable(data, 'a'):
    ...     dotted.update(data, 'a', 2)  # mutates in place
    ... else:
    ...     data = dotted.update(data, 'a', 2)  # use return value

<a id="setdefault"></a>
### Setdefault

Set a value only if the key doesn't already exist. Creates nested structures as needed.

    >>> import dotted
    >>> d = {'a': 1}
    >>> dotted.setdefault(d, 'a', 999)  # key exists, no change; returns value
    1
    >>> dotted.setdefault(d, 'b', 2)    # key missing, sets value; returns it
    2
    >>> dotted.setdefault({}, 'a.b.c', 7)  # creates nested structure; returns value
    7

<a id="pluck"></a>
### Pluck

Extract (key, value) pairs from an object matching a pattern.

    >>> import dotted
    >>> d = {'a': 1, 'b': 2, 'nested': {'x': 10}}
    >>> dotted.pluck(d, 'a')
    ('a', 1)
    >>> dotted.pluck(d, '*')
    (('a', 1), ('b', 2), ('nested', {'x': 10}))
    >>> dotted.pluck(d, 'nested.*')
    (('nested.x', 10),)

<a id="unpack"></a>
### Unpack

Recursively unpack a nested structure into `(path, value)` pairs — its dotted
normal form. The result can be replayed with `update_multi` to reconstruct the
original object. Use `AUTO` as the base object to infer the root container type.

    >>> import dotted
    >>> d = {'a': {'b': [1, 2, 3]}, 'x': {'y': {'z': [4, 5]}}, 'extra': 'stuff'}
    >>> dotted.unpack(d)
    (('a.b', [1, 2, 3]), ('x.y.z', [4, 5]), ('extra', 'stuff'))
    >>> dotted.update_multi(dotted.AUTO, dotted.unpack(d)) == d
    True

<a id="build"></a>
### Build

Create a default nested structure for a dotted key.

    >>> import dotted
    >>> dotted.build({}, 'a.b.c')
    {'a': {'b': {'c': None}}}
    >>> dotted.build({}, 'items[]')
    {'items': []}
    >>> dotted.build({}, 'items[0]')
    {'items': [None]}

<a id="apply"></a>
### Apply

Apply transforms to values in an object in-place.

    >>> import dotted
    >>> d = {'price': '99.99', 'quantity': '5'}
    >>> dotted.apply(d, 'price|float')
    {'price': 99.99, 'quantity': '5'}
    >>> dotted.apply(d, '*|int')
    {'price': 99, 'quantity': 5}

<a id="assemble"></a>
### Assemble

Build a dotted notation string from a list of path segments.

    >>> import dotted
    >>> dotted.assemble(['a', 'b', 'c'])
    'a.b.c'
    >>> dotted.assemble(['items', '[0]', 'name'])
    'items[0].name'
    >>> dotted.assemble([7, 'hello'])
    '7.hello'

<a id="quote"></a>
### Quote

Quote a key for use in a dotted path. Wraps in single quotes if the key
contains reserved characters or whitespace.

    >>> import dotted
    >>> dotted.quote('hello')
    'hello'
    >>> dotted.quote('has.dot')
    "'has.dot'"
    >>> dotted.quote('has space')
    "'has space'"
    >>> dotted.quote(7)
    '7'
    >>> dotted.quote('7')
    '7'

<a id="normalize"></a>
### Normalize

Convert a raw Python key to dotted normal form. Like `quote()`, but also
quotes string keys that look numeric so they round-trip correctly through
`unpack`/`update_multi` (preserving string vs int key type).

    >>> import dotted
    >>> dotted.normalize('hello')
    'hello'
    >>> dotted.normalize('has.dot')
    "'has.dot'"
    >>> dotted.normalize(7)
    '7'
    >>> dotted.normalize('7')
    "'7'"

`unpack` uses `normalize` internally, so dotted normal form paths always
round-trip correctly:

    >>> d = {'7': 'seven', 'a.b': 'dotted', 'hello': 'world'}
    >>> flat = dict(dotted.unpack(d))
    >>> dotted.update_multi(dotted.AUTO, flat.items()) == d
    True

<a id="auto"></a>
### AUTO

All write operations (`update`, `update_multi`, `build`, `setdefault`, etc.) accept
`AUTO` as the base object. Instead of passing `{}` or `[]`, let dotted infer
the root container type from the first key — dict keys produce `{}`, slot keys
produce `[]`.

    >>> import dotted
    >>> dotted.update(dotted.AUTO, 'a.b', 1)
    {'a': {'b': 1}}
    >>> dotted.update(dotted.AUTO, '[0]', 'hello')
    ['hello']
    >>> dotted.update_multi(dotted.AUTO, [('a', 1), ('b', 2)])
    {'a': 1, 'b': 2}

<a id="multi-operations"></a>
### Multi Operations

Most operations have `*_multi` variants for batch processing:

**Note:** `get_multi` returns a generator (not a list or tuple). That distinguishes it from a pattern `get`, which returns a tuple of matches. It also keeps input and output in the same style when you pass an iterator or generator of paths—lazy in, lazy out.

    >>> import dotted
    >>> d = {'a': 1, 'b': 2, 'c': 3}
    >>> list(dotted.get_multi(d, ['a', 'b']))
    [1, 2]
    >>> dotted.update_multi({}, [('a.b', 1), ('c.d', 2)])
    {'a': {'b': 1}, 'c': {'d': 2}}
    >>> dotted.remove_multi(d, ['a', 'c'])
    {'b': 2}
    >>> d = {'a': 1}; list(dotted.setdefault_multi(d, [('a', 999), ('b', 2)]))
    [1, 2]
    >>> d
    {'a': 1, 'b': 2}
    >>> dotted.update_if_multi({}, [('a', 1), ('b', None), ('c', 3)])  # skips None vals
    {'a': 1, 'c': 3}
    >>> dotted.remove_if_multi({'a': 1, 'b': 2}, ['a', None, 'b'])  # skips None keys
    {}

Available multi operations: `get_multi`, `update_multi`, `update_if_multi`, `remove_multi`,
`remove_if_multi`, `setdefault_multi`, `match_multi`, `expand_multi`, `apply_multi`,
`build_multi`, `pluck_multi`, `assemble_multi`.

<a id="paths"></a>
## Paths

Dotted notation shares similarities with python. A _dot_ `.` field expects to see a
dictionary-like object (using `keys` and `__getitem__` internally).  A _bracket_ `[]`
field is biased towards sequences (like lists or strs) but can also act on dicts.  A
_attr_ `@` field uses `getattr/setattr/delattr`.

<a id="key-fields"></a>
### Key fields

A key field is expressed as `a` or part of a dotted expression, such as `a.b`.  The
grammar parser is permissive for what can be in a key field.  Pretty much any non-reserved
char will match.  Note that key fields will only work on objects that have a `keys`
method.  Basically, they work with dictionary or dictionary-like objects.

    >>> import dotted
    >>> dotted.get({'a': {'b': 'hello'}}, 'a.b')
    'hello'

If the key field starts with a space or `-`, you should either quote it OR you may use
a `\` as the first char.

<a id="bracketed-fields"></a>
### Bracketed fields

You may also use bracket notation, such as `a[0]` which does a `__getitem__` at key 0.
The parser prefers numeric types over string types (if you wish to look up a non-numeric
field using brackets be sure to quote it).  Bracketed fields will work with pretty much
any object that can be looked up via `__getitem__`.

    >>> import dotted
    >>> dotted.get({'a': ['first', 'second', 'third']}, 'a[0]')
    'first'
    >>> dotted.get({'a': {'b': 'hello'}}, 'a["b"]')
    'hello'

<a id="attr-fields"></a>
### Attr fields

An attr field is expressed by prefixing with `@`. This will fetch data at that attribute.
You may wonder why have this when you can just as easily use standard python to access.
Two important reasons: nested expressions and patterns.

    >>> import dotted, types
    >>> ns = types.SimpleNamespace()
    >>> ns.hello = {'me': 'goodbye'}
    >>> dotted.get(ns, '@hello.me')
    'goodbye'

<a id="slicing"></a>
### Slicing

Dotted slicing works like python slicing and all that entails.

    >>> import dotted
    >>> d = {'hi': {'there': [1, 2, 3]}, 'bye': {'there': [4, 5, 6]}}
    >>> dotted.get(d, 'hi.there[::2]')
    [1, 3]
    >>> dotted.get(d, '*.there[1:]')
    ([2, 3], [5, 6])

<a id="dot-notation-for-sequence-indexing"></a>
### Dot notation for sequence indexing

Numeric path segments work as indices when accessing sequences (lists, tuples, strings):

    >>> import dotted
    >>> data = {'items': [10, 20, 30]}
    >>> dotted.get(data, 'items.0')
    10
    >>> dotted.get(data, 'items.-1')  # negative index
    30

This is equivalent to bracket notation for existing sequences:

    >>> dotted.get(data, 'items[0]')  # same result
    10

Chaining works naturally:

    >>> data = {'users': [{'name': 'alice'}, {'name': 'bob'}]}
    >>> dotted.get(data, 'users.0.name')
    'alice'

Updates and removes also work:

    >>> dotted.update(data, 'users.0.name', 'ALICE')
    >>> dotted.get(data, 'users.0.name')
    'ALICE'

**Note**: When _creating_ structures, use bracket notation for lists:

    >>> dotted.build({}, 'items.0')    # creates dict: {'items': {0: None}}
    >>> dotted.build({}, 'items[0]')   # creates list: {'items': [None]}

<a id="empty-path-root-access"></a>
### Empty path (root access)

An empty string `''` refers to the root of the data structure itself:

    >>> import dotted
    >>> data = {'a': 1, 'b': 2}
    >>> dotted.get(data, '')
    {'a': 1, 'b': 2}

Unlike normal paths which mutate in place, `update` with an empty path is non-mutating
since Python cannot rebind the caller's variable:

    >>> data = {'a': 1, 'b': 2}
    >>> result = dotted.update(data, '', {'replaced': True})
    >>> result
    {'replaced': True}
    >>> data
    {'a': 1, 'b': 2}

Compare with a normal path which mutates:

    >>> data = {'a': 1, 'b': 2}
    >>> dotted.update(data, 'a', 99)
    {'a': 99, 'b': 2}
    >>> data
    {'a': 99, 'b': 2}

Other empty path operations:

    >>> data = {'a': 1, 'b': 2}
    >>> dotted.remove(data, '')
    None
    >>> dotted.expand(data, '')
    ('',)
    >>> dotted.pluck(data, '')
    ('', {'a': 1, 'b': 2})

<a id="typing--quoting"></a>
## Typing & Quoting

<a id="numeric-types"></a>
### Numeric types

The parser will attempt to interpret a field numerically if it can, such as `field.1`
will interpret the `1` part numerically.

    >>> import dotted
    >>> dotted.get({'7': 'me', 7: 'you'}, '7')
    'you'

<a id="quoting"></a>
### Quoting

Sometimes you need to quote a field which you can do by just putting the field in quotes.

    >>> import dotted
    >>> dotted.get({'has . in it': 7}, '"has . in it"')
    7

<a id="the-numericize--operator"></a>
### The numericize `#` operator

Non-integer numeric fields may be interpreted incorrectly if they have decimal point. To
solve, use the numerize operator `#` at the front of a quoted field, such as `#'123.45'`.
This will coerce to a numeric type (e.g. float).

    >>> import dotted
    >>> d = {'a': {1.2: 'hello', 1: {2: 'fooled you'}}}
    >>> dotted.get(d, 'a.1.2')
    'fooled you'
    >>> dotted.get(d, 'a.#"1.2"')
    'hello'

<a id="container-types"></a>
### Container types

Container literals express list, dict, set, tuple, and frozenset values inline. They
appear in two contexts: as [filter values / value guards](#container-filter-values)
(with pattern support) and as [transform arguments](#container-transform-arguments)
(concrete values only).

| Syntax | Type | Notes |
|--------|------|-------|
| `[1, 2, 3]` | list or tuple | Unprefixed matches both |
| `l[1, 2, 3]` | list | Strict: list only |
| `t[1, 2, 3]` | tuple | Strict: tuple only |
| `{"a": 1}` | dict | Unprefixed matches dict-like |
| `d{"a": 1}` | dict | Strict: dict only (isinstance) |
| `{1, 2, 3}` | set or frozenset | Unprefixed matches both |
| `s{1, 2, 3}` | set | Strict: set only |
| `fs{1, 2, 3}` | frozenset | Strict: frozenset only |

Empty containers: `[]` (empty list/tuple), `{}` (empty dict), `s{}` (empty set),
`fs{}` (empty frozenset), `l[]`, `t[]`, `d{}`.

Without a [type prefix](#type-prefixes), brackets match loosely: `[]` matches any
list or tuple, `{v, v}` matches any set or frozenset, `{}` matches dict (following
Python convention where `{}` is a dict literal, not a set).

In filter context, containers support patterns (`*`, `...`, `/regex/`) inside — see
[Container filter values](#container-filter-values). In transform argument context,
only concrete scalar values are allowed.

<a id="bytes-literals"></a>
### Bytes literals

Prefix a quoted string with `b` to create a bytes literal: `b"hello"` or `b'hello'`.
Bytes literals produce `bytes` values and only match `bytes` — never `str`:

    >>> import dotted
    >>> d = {'a': b'hello', 'b': b'world', 'c': 'hello'}
    >>> dotted.get(d, '*=b"hello"')
    (b'hello',)

Use in filters:

    >>> data = [{'data': b'yes'}, {'data': b'no'}, {'data': 'yes'}]
    >>> dotted.get(data, '[*&data=b"yes"]')
    ({'data': b'yes'},)

Note that `b"hello"` does not match the string `'hello'` — types must match exactly.

<a id="patterns"></a>
## Patterns

You may use dotted for pattern matching. You can match to wildcards or regular
expressions.  You'll note that patterns always return a tuple of matches.

    >>> import dotted
    >>> d = {'hi': {'there': [1, 2, 3]}, 'bye': {'there': [4, 5, 6]}}
    >>> dotted.get(d, '*.there[2]')
    (3, 6)
    >>> dotted.get(d, '/h.*/.*')
    ([1, 2, 3],)

Dotted will return all values that match the pattern(s).

<a id="wildcards"></a>
### Wildcards

The wildcard pattern is `*`.  It will match anything.

<a id="regular-expressions"></a>
### Regular expressions

The regex pattern is enclosed in slashes: `/regex/`. Note that if the field is a non-str,
the regex pattern will internally match to its str representation.

<a id="the-match-first-operator"></a>
### The match-first operator

You can also postfix any pattern with a `?`.  This will return only
the first match.

    >>> import dotted
    >>> d = {'hi': {'there': [1, 2, 3]}, 'bye': {'there': [4, 5, 6]}}
    >>> dotted.get(d, '*?.there[2]')
    (3,)

<a id="slicing-vs-patterns"></a>
### Slicing vs Patterns

Slicing a sequence produces a sequence and a filter on a sequence is a special
type of slice operation. Whereas, patterns _iterate_ through items:

    >>> import dotted
    >>> data = [{'name': 'alice'}, {'name': 'bob'}, {'name': 'alice'}]
    >>> dotted.get(data, '[1:3]')
    [{'name': 'bob'}, {'name': 'alice'}]
    >>> dotted.get(data, '[name="alice"]')
    [{'name': 'alice'}, {'name': 'alice'}]
    >>> dotted.get(data, '[*]')
    ({'name': 'alice'}, {'name': 'bob'}, {'name': 'alice'})

Chaining after a slice accesses the result itself, not the items within it:

    >>> dotted.get(data, '[1:3].name')           # accessing .name on the list
    None
    >>> dotted.get(data, '[name="alice"].name')  # also accessing .name on the list
    None
    >>> dotted.get(data, '[].name')              # .name on a raw list
    None

To chain through the items, use a pattern instead:

    >>> dotted.get(data, '[*].name')
    ('alice', 'bob', 'alice')
    >>> dotted.get(data, '[*&name="alice"]')
    ({'name': 'alice'}, {'name': 'alice'})

<a id="recursive-traversal"></a>
## Recursive Traversal

The recursive operator `*` traverses nested data structures by following path
segments that match a pattern at successive levels.

<a id="the-recursive-operator-"></a>
### The recursive operator `*`

`*pattern` recurses into values whose path segments match the pattern. It follows
chains of matching segments — at each level, if a segment matches, its value is
yielded and the traversal continues into that value:

    >>> import dotted
    >>> d = {'b': {'b': {'c': 1}}}
    >>> dotted.get(d, '*b')
    ({'b': {'c': 1}}, {'c': 1})
    >>> dotted.get(d, '*b.c')
    (1,)

The chain stops when the key no longer matches:

    >>> d = {'a': {'b': {'c': 1}}}
    >>> dotted.get(d, '*b')
    ()

The inner pattern can be any key pattern — a literal key, a wildcard, or a regex:

    >>> d = {'x1': {'x2': 1}, 'y': 2}
    >>> dotted.get(d, '*/x.*/')
    ({'x2': 1}, 1)

<a id="recursive-wildcard-"></a>
### Recursive wildcard `**`

`**` is the recursive wildcard — it matches all path segments and visits every
value at every depth:

    >>> d = {'a': {'b': {'c': 1}}, 'x': {'y': 2}}
    >>> dotted.get(d, '**')
    ({'b': {'c': 1}}, {'c': 1}, 1, {'y': 2}, 2)

Use `**` with continuation to find a key at any depth:

    >>> dotted.get(d, '**.c')
    (1,)

Use `**?` to get only the first match:

    >>> dotted.get(d, '**?')
    ({'b': {'c': 1}},)

<a id="depth-slicing"></a>
### Depth slicing

Control which depths are visited using slice notation: `**:start`, `**:start:stop`,
or `**:::step`. Note the leading `:` — depth slicing looks a little different from
regular Python slicing since it follows the `**` operator. Depth 0 is the values of
the first-level path segments. Lists increment depth (their elements are one level deeper).

    >>> d = {'a': {'x': 1}, 'b': {'y': {'z': 2}}}
    >>> dotted.get(d, '**:0')
    ({'x': 1}, {'y': {'z': 2}})
    >>> dotted.get(d, '**:1')
    (1, {'z': 2})

Use negative indices to count from the leaf. `**:-1` returns leaves only,
`**:-2` returns the penultimate level:

    >>> dotted.get(d, '**:-1')
    (1, 2)

Range slicing works like Python slices: `**:start:stop` and `**:::step`:

    >>> dotted.get(d, '**:0:1')
    ({'x': 1}, 1, {'y': {'z': 2}}, {'z': 2})

<a id="recursive-with-value-guard"></a>
### Recursive with value guard

Combine `**` with value guards to find specific values at any depth:

    >>> d = {'a': {'b': 7, 'c': 3}, 'd': {'e': 7}}
    >>> dotted.get(d, '**=7')
    (7, 7)
    >>> dotted.get(d, '**!=7')
    ({'b': 7, 'c': 3}, 3, {'e': 7})

<a id="recursive-update-and-remove"></a>
### Recursive update and remove

Recursive operators work with `update` and `remove`:

    >>> d = {'a': {'b': 7, 'c': 3}, 'd': 7}
    >>> dotted.update(d, '**=7', 99)
    {'a': {'b': 99, 'c': 3}, 'd': 99}

    >>> d = {'a': {'b': 7, 'c': 3}, 'd': 7}
    >>> dotted.remove(d, '**=7')
    {'a': {'c': 3}}

<a id="recursive-match"></a>
### Recursive match

Recursive patterns work with `match`. `**` matches any key path, `*key` matches
chains of a specific key:

    >>> dotted.match('**.c', 'a.b.c')
    'a.b.c'
    >>> dotted.match('*b', 'b.b.b')
    'b.b.b'
    >>> dotted.match('*b', 'a.b.c') is None
    True

<a id="grouping"></a>
## Grouping

<a id="path-grouping"></a>
### Path grouping

Use parentheses to group keys that share a common prefix or suffix:

    >>> import dotted
    >>> d = {'a': 1, 'b': 2, 'c': 3}

    # Group keys
    >>> dotted.get(d, '(a,b)')
    (1, 2)

    # With a shared suffix
    >>> d = {'x': {'val': 1}, 'y': {'val': 2}}
    >>> dotted.get(d, '(x,y).val')
    (1, 2)

Path grouping is syntactic sugar for [operation grouping](#operation-grouping)
where each branch is a single key—`(a,b).c` is equivalent to `(.a,.b).c`. All
operators (disjunction, conjunction, negation, cut `#`, soft cut `##`,
first-match `?`) work the same way; see operation grouping for full details.

<a id="operation-grouping"></a>
### Operation grouping

Use parentheses to group **operation sequences** that diverge from a common point.
Each branch is a full operation chain including dots, brackets, and attrs:

    >>> import dotted

    # Mix different operation types from a common prefix
    >>> d = {'items': [10, 20, 30]}
    >>> dotted.get(d, 'items(.0,[])')
    (10, [10, 20, 30])

    # Nested paths in branches
    >>> d = {'x': {'a': {'i': 1}, 'b': {'k': 3}}}
    >>> dotted.get(d, 'x(.a.i,.b.k)')
    (1, 3)

Operation groups support these operators:

| Syntax | Meaning | Behavior |
|--------|---------|----------|
| `(.a,.b)` | Disjunction (OR) | Returns all values that exist |
| `(.a&.b)` | Conjunction (AND) | Returns values only if ALL branches exist |
| `(!.a)` | Negation (NOT) | Returns values for keys NOT matching |

#### Disjunction (OR)

Comma separates branches. Returns all matches that exist. Disjunction doesn't
short-circuit—when updating, all matching branches get the update. Using the
match-first operator (`?`) is probably what you want when updating.

    >>> d = {'a': {'x': 1, 'y': 2}}
    >>> dotted.get(d, 'a(.x,.y)')
    (1, 2)
    >>> dotted.get(d, 'a(.x,.z)')     # z missing, x still returned
    (1,)

Updates apply to all matching branches. When nothing matches, the first
concrete path (scanning last to first) is created:

    >>> d = {'a': {'x': 1, 'y': 2}}
    >>> dotted.update(d, 'a(.x,.y)', 99)
    {'a': {'x': 99, 'y': 99}}
    >>> dotted.update({'a': {}}, 'a(.x,.y)', 99)   # nothing matches → creates last (.y)
    {'a': {'y': 99}}

#### Cut (`#`) in disjunction

Suffix a branch with `#` so that if it matches, only that branch is used
(get/update/remove); later branches are not tried. Useful for "update if exists,
else append" in lists. Example with slot grouping:

    >>> data = {'emails': [{'email': 'alice@x.com', 'verified': False}]}
    >>> dotted.update(data, 'emails[(*&email="alice@x.com"#, +)]', {'email': 'alice@x.com', 'verified': True})
    {'emails': [{'email': 'alice@x.com', 'verified': True}]}
    >>> data = {'emails': [{'email': 'other@x.com'}]}
    >>> dotted.update(data, 'emails[(*&email="alice@x.com"#, +)]', {'email': 'alice@x.com', 'verified': True})
    {'emails': [{'email': 'other@x.com'}, {'email': 'alice@x.com', 'verified': True}]}

First branch matches items where `email="alice@x.com"` and updates them (then cut);
if none match, the `+` branch appends the new dict.

#### Soft cut (`##`) in disjunction

Hard cut (`#`) stops all later branches when the cut branch matches. Soft cut (`##`)
is more selective: later branches still run, but skip any paths that overlap with
what the soft-cut branch already yielded. Use soft cut when a branch handles some
keys and you want a fallback branch to handle the rest.

    >>> d = {'a': {'b': [1, 2, 3]}, 'x': {'y': {'z': [4, 5]}}, 'extra': 'stuff'}
    >>> dotted.pluck(d, '(**:-2(.*, [])##, *)')
    (('a.b', [1, 2, 3]), ('x.y.z', [4, 5]), ('extra', 'stuff'))

Here `**:-2(.*, [])` recurses into containers (dicts and lists) and yields their
leaf containers. The `##` means: for keys that recursion covered (like `a` and `x`),
don't try the `*` fallback. But `extra` was not covered by the recursive branch,
so `*` picks it up.

Compare with hard cut (`#`), which would lose `extra` entirely:

    >>> dotted.pluck(d, '(**:-2(.*, [])#, *)')
    (('a.b', [1, 2, 3]), ('x.y.z', [4, 5]))

#### Conjunction (AND)

Use `&` for all-or-nothing behavior. Returns values only if ALL branches exist:

    >>> d = {'a': {'x': 1, 'y': 2}}
    >>> dotted.get(d, 'a(.x&.y)')
    (1, 2)
    >>> dotted.get(d, 'a(.x&.z)')     # z missing, fails entirely
    ()

Updates all branches so the conjunction eval as true—creates missing paths.
If a filter or NOP prevents a branch, no update:

    >>> dotted.update({'a': {'x': 1, 'y': 2}}, 'a(.x&.y)', 99)
    {'a': {'x': 99, 'y': 99}}
    >>> dotted.update({'a': {'x': 1}}, 'a(.x&.y)', 99)    # y missing → creates it
    {'a': {'x': 99, 'y': 99}}

#### First match

Use `?` suffix to return only the first match. When nothing matches, same
fallback as disjunction—first concrete path (last to first):

    >>> d = {'a': {'x': 1, 'y': 2}}
    >>> dotted.get(d, 'a(.z,.x,.y)?')    # first that exists
    (1,)
    >>> dotted.update({'a': {}}, 'a(.x,.y)?', 99)    # nothing matches → creates last (.y)
    {'a': {'y': 99}}

#### Negation (NOT)

Use `!` prefix to exclude keys matching a pattern:

    >>> import dotted

    # Exclude single key - get user fields except password
    >>> user = {'email': 'a@x.com', 'name': 'alice', 'password': 'secret'}
    >>> sorted(dotted.get({'user': user}, 'user(!.password)'))
    ['a@x.com', 'alice']

    # Works with lists too
    >>> dotted.get({'items': [10, 20, 30]}, 'items(![0])')
    (20, 30)

Updates and removes apply to all non-matching keys:

    >>> d = {'a': {'x': 1, 'y': 2, 'z': 3}}
    >>> dotted.update(d, 'a(!.x)', 99)
    {'a': {'x': 1, 'y': 99, 'z': 99}}
    >>> dotted.remove(d, 'a(!.x)')
    {'a': {'x': 1}}

**Note**: For De Morgan's law with filter expressions, see the Filters section below.

<a id="operators"></a>
## Operators

<a id="the-append--operator"></a>
### The append `+` operator

Both bracketed fields and slices support the '+' operator which refers to the end of
sequence. You may append an item or slice to the end a sequence.

    >>> import dotted
    >>> d = {'hi': {'there': [1, 2, 3]}, 'bye': {'there': [4, 5, 6]}}
    >>> dotted.update(d, '*.there[+]', 8)
    {'hi': {'there': [1, 2, 3, 8]}, 'bye': {'there': [4, 5, 6, 8]}}
    >>> dotted.update(d, '*.there[+:]', [999])
    {'hi': {'there': [1, 2, 3, 8, 999]}, 'bye': {'there': [4, 5, 6, 8, 999]}}

<a id="the-append-unique--operator"></a>
### The append-unique `+?` operator

If you want to update only _unique_ items to a list, you can use the `?`
postfix.  This will ensure that it's only added once (see match-first below).

    >>> import dotted
    >>> items = [1, 2]
    >>> dotted.update(items, '[+?]', 3)
    [1, 2, 3]
    >>> dotted.update(items, '[+?]', 3)
    [1, 2, 3]

<a id="the-invert---operator"></a>
### The invert `-` operator

You can invert the meaning of the notation by prefixing a `-`.  For example,
to remove an item using `update`:

    >>> import dotted
    >>> d = {'a': 'hello', 'b': 'bye'}
    >>> dotted.update(d, '-b', dotted.ANY)
    {'a': 'hello'}
    >>> dotted.remove(d,