Skip to main content

A lazy selector for nested Python data structures.

Project description

dictselect

A lightweight Python library for extracting data from nested dicts and lists using composable, reusable selectors.

Installation

pip install dictselect

Requires Python ≥ 3.9.

Quick start

from dictselect import Selector

data = {
    "image_id": "xa001",
    "annotations": [
        {"id": 1, "x_min": 10, "x_max": 20, "label": "cat"},
        {"id": 2, "x_min": 30, "x_max": 50, "label": "dog"},
    ],
}

Selector["image_id"](data)                          # → "xa001"
Selector["annotations"][0]["label"](data)           # → "cat"
Selector["annotations"][:]["label"](data)           # → ["cat", "dog"]
Selector["annotations"][:]["x_min", "x_max"](data)  # → [[10, 20], [30, 50]]

Build a selector once, apply it to many objects:

sel = Selector["annotations"][:]["x_min", "x_max"]
sel(record_a)
sel(record_b)

Operations

Syntax What it does
Selector["key"] Dict key or list index lookup
Selector[0], Selector[-1] List index (positive or negative)
Selector[1:3] Slice — returns a sub-list or sub-string
Selector[:] or Selector[...] Fan-out — map remaining steps over every element
Selector["a", "b"] Pluck multiple keys at once, returns a list
Selector.attr Attribute access (getattr)
Selector.method(args) Attribute access followed by a call
sel_a + sel_b Compose two selectors
sel(data, include_keys=True) Wrap result in a dict keyed by the last access key
sel(data, include_null=True) Return None instead of raising on missing keys
Selector[{"alias": "key"}] Aliased key — access via "key", label as "alias"
Selector["fn"].invoke(*args) Call a callable value stored in data

Usage

Key and index lookup

Selector["name"]({"name": "Alice"})   # → "Alice"
Selector[0]([10, 20, 30])             # → 10
Selector[-1]([10, 20, 30])            # → 30
Selector["a"]["b"]({"a": {"b": 7}})  # → 7

Slicing

Selector[1:3]([0, 1, 2, 3, 4])  # → [1, 2]
Selector[:2]([10, 20, 30])       # → [10, 20]
Selector[-2:]([10, 20, 30])      # → [20, 30]
Selector[::2]([0, 1, 2, 3, 4])  # → [0, 2, 4]

Fan-out [:]

[:] (or [...]) maps all subsequent steps over every element of the current sequence. Steps after [:] run on each element individually.

data = [{"v": 1}, {"v": 2}, {"v": 3}]

Selector[:]["v"](data)   # → [1, 2, 3]
Selector[:][0]([[10, 20], [30, 40]])  # → [10, 30]

Plucking multiple keys

Pass a tuple or list of keys to retrieve several fields at once. All keys must be the same type (all str or all int). Returns a list of values in the order given.

Selector["x", "y"]({"x": 1, "y": 2, "z": 3})  # → [1, 2]
Selector[0, 2]([10, 20, 30, 40])               # → [10, 30]

Combined with fan-out:

Selector["annotations"][:]["x_min", "x_max"](data)
# → [[10, 20], [30, 50]]

Attribute access and method calls

Chain attribute access with .attr, then call it like a regular Python method:

Selector.upper()("hello")                   # → "HELLO"
Selector.replace("l", "r")("hello")        # → "herro"
Selector[:].upper()(["hi", "there"])       # → ["HI", "THERE"]
Selector["title"].upper()({"title": "hi"}) # → "HI"

Composing selectors

Join two selectors with +. Neither operand is mutated:

head = Selector["data"][:]
tail = Selector["value"]
sel = head + tail
sel({"data": [{"value": 1}, {"value": 2}]})  # → [1, 2]

Calling vs. evaluating

Normally, calling a selector evaluates it against the data:

sel = Selector["key"]
sel({"key": 42})       # → 42
sel.apply({"key": 42}) # same thing

Attribute-access exception: if the last recorded step is an attribute name, calling the selector records a method-call step instead of evaluating. Use .apply() to force evaluation:

Selector["title"].upper()           # records the .upper() call — returns a new Selector
Selector["title"].upper()(data)     # evaluates → "HELLO"

Selector.upper.apply("hello")       # force-evaluates → the bound method object (not "HELLO")

Calling a function stored in data: accessing a callable value via ["key"] and then calling with () would evaluate the selector (returning the function itself), not invoke it. Use .invoke() to record a function call as a step:

data = {"fn": lambda x: x * 2}

Selector["fn"](data)            # → the lambda object (selector evaluated, function not called)
Selector["fn"].invoke(21)(data) # → 42 (function is called with 21 at evaluation time)

Returning keys alongside values

Pass include_keys=True to wrap the result in a dict keyed by the last access key.

Selector["a"]["b"]({"a": {"b": 12}}, include_keys=True)
# → {"b": 12}

Selector["x"].apply({"x": 7}, include_keys=True)
# → {"x": 7}

With fan-out, wrapping happens per element:

Selector[:]["a"]([{"a": 1}, {"a": 2}], include_keys=True)
# → [{"a": 1}, {"a": 2}]

Selector[:]["a", "b"]([{"a": 1, "b": 2, "c": 3}, {"a": 4, "c": 6, "b": 5}], include_keys=True)
# → [{"a": 1, "b": 2}, {"a": 4, "b": 5}]

Terminal steps that are not key lookups (slices, method calls) pass through unchanged.

Aliasing output keys

Use a single-entry dict {"alias": "key"} to rename a field in the output. The dict key is the output name, the dict value is the access key. Plain keys keep their original name.

Selector[{"alias_a": "a"}]({"a": 7}, include_keys=True)
# → {"alias_a": 7}

Selector[{"alias_a": "a"}, "b", {"alias_c": "c"}](
    {"a": 1, "b": 2, "c": 3}, include_keys=True
)
# → {"alias_a": 1, "b": 2, "alias_c": 3}

Without include_keys, aliases are ignored and raw values are returned as usual.

Sub-selector aliases

The access value in an alias dict can itself be a Selector. It is applied to the current data and stored under the given alias. A bare Selector without an alias is not allowed.

name_sel = Selector["name"]["first_name", "last_name"]

Selector["employees"][:][{"name": name_sel}, "adress"](data, include_keys=True)
# → [
#     {"name": ["Alice", "Smith"], "adress": "1 Main St"},
#     {"name": ["Bob",   "Jones"], "adress": "2 Oak Ave"},
# ]

Handling missing values

Pass include_null=True to return None instead of raising KeyError or IndexError when a key or index is absent. Once a step fails, the rest of the chain is skipped.

Selector["a"]["missing"]({"a": {}}, include_null=True)
# → None

Selector[:]["x"]([{"x": 1}, {"y": 2}, {"x": 3}], include_null=True)
# → [1, None, 3]

Selector["a", "b"]({"a": 1}, include_null=True)
# → [1, None]  (each missing key becomes None individually)

Both flags can be combined:

Selector[:]["x"]([{"x": 1}, {"y": 2}], include_null=True, include_keys=True)
# → [{"x": 1}, {"x": None}]

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dictselect-0.2.0.tar.gz (14.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dictselect-0.2.0-py3-none-any.whl (13.7 kB view details)

Uploaded Python 3

File details

Details for the file dictselect-0.2.0.tar.gz.

File metadata

  • Download URL: dictselect-0.2.0.tar.gz
  • Upload date:
  • Size: 14.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dictselect-0.2.0.tar.gz
Algorithm Hash digest
SHA256 8a530b4378eca7060fa30740c055b3fba85ba06eda4b35768d2128d2f6dcea69
MD5 a36c698d065919ed3b90cbabf8f84e72
BLAKE2b-256 e06c88178ed2b740072772ad6484e6743728a5636dde84bc69d107c358f722c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for dictselect-0.2.0.tar.gz:

Publisher: publish.yml on alphacena/dictselect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dictselect-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: dictselect-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dictselect-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 275e0019cea68514323fb47fc0e254f1469265338729ee5fa4171caebb3bb47b
MD5 164a6070fd2c4c4a076f351301602e7c
BLAKE2b-256 87e632cac5ed6a8839d010cfd93573127f81ff0222c48797fc854b06d7ed6985

See more details on using hashes here.

Provenance

The following attestation bundles were made for dictselect-0.2.0-py3-none-any.whl:

Publisher: publish.yml on alphacena/dictselect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page