Skip to main content

dictutils

A collection of utilities for manipulating nested dictionaries and data structures in Python.

Documentation Status CI

Requirements

  • Python 3.9+

Installation

Install from PyPI (recommended):

pip install dictutils

Or from source (using PEP 517/518):

pip install .
# For contributors (editable install):
pip install -e .[test,typecheck,lint]

Quick Examples

from dictutils import qsdict, mergedict, pivot, nest_agg, Agg

# Build nested dicts from lists
sales = [{"region": "North", "product": "Widget", "revenue": 1000}]
result = qsdict(sales, "region", "product", "revenue")
# {"North": {"Widget": 1000}}

# Safely merge nested dicts
a = {"user": {"name": "Alice"}}
b = {"user": {"email": "alice@example.com"}}
mergedict(a, b)  # {"user": {"name": "Alice", "email": "alice@example.com"}}

# Pivot nested structures  
data = {"A": {"X": 1, "Y": 2}, "B": {"X": 3, "Y": 4}}
pivot(data, [1, 0])  # {"X": {"A": 1, "B": 3}, "Y": {"A": 2, "B": 4}}

# Advanced aggregation
aggs = {"total": Agg(map=lambda x: x["amount"], zero=0)}
nest_agg(transactions, keys=["category"], aggs=aggs)

API

dictutils.qsdict

def qsdict(qs: Iterable[Union[Mapping, object]], *args, strict: bool = False) -> dict:
    """Build a nested dict from rows (dicts or objects) by a sequence of selectors."""

Example

from dictutils import qsdict
lst = [
    {"shape": "circle", "colour": "blue", "count": 5},
    {"shape": "circle", "colour": "pink", "count": 15},
    {"shape": "square", "colour": "yellow", "count": 29},
    {"shape": "square", "colour": "blue", "count": 10},
]
result = qsdict(lst, "shape", "colour", "count")
# {'circle': {'blue': 5, 'pink': 15}, 'square': {'yellow': 29, 'blue': 10}}

# Strict mode raises on missing keys/attributes
qsdict([{"a": 1}], "a", "missing", strict=True)  # KeyError

dictutils.mergedict

def mergedict(*args, path=None, update=True) -> dict:
    """Merge multiple nested dicts. The first dict is updated in-place."""

Example

from dictutils import mergedict
d1 = {"a": {"b": 1}}
d2 = {"a": {"c": 2}}
merged = mergedict(d1, d2)
# d1 is now {"a": {"b": 1, "c": 2}}

dictutils.pivot

def pivot(d: dict, order: list[int]) -> dict:
    """Pivot a nested dict by a list of key indices."""

Example

from dictutils import pivot
d = {"A": {"X": 1, "Y": 2}, "B": {"X": 3, "Y": 4}}
result = pivot(d, [1, 0])
# {'X': {'A': 1, 'B': 3}, 'Y': {'A': 2, 'B': 4}}

dictutils.nestagg

from typing import Any, Callable
from dataclasses import dataclass

def nest_agg(
    items: list[Any],
    keys: list[str | Callable[[Any], Any]],
    *,
    aggs: dict[str, 'Agg'],
    include_rows: bool = False,
    rows_key: str = "rows",
) -> dict:
    """Group and aggregate items by keys, with flexible aggregation at leaves."""

@dataclass(frozen=True)
class Agg:
    map: Callable[[Any], Any]
    zero: Any | Callable[[], Any] | None = None
    reduce: Callable[[Any, Any], Any] = operator.add
    skip_none: bool = True
    finalize: Callable[[Any], Any] | None = None  # Transform final result

Examples

from dictutils import nest_agg, Agg

# Simple aggregation
items = [{"cat": "A", "val": 1}, {"cat": "A", "val": 2}, {"cat": "B", "val": 3}]
aggs = {"total": Agg(map=lambda it: it["val"], zero=0)}
result = nest_agg(items, keys=["cat"], aggs=aggs)
# {'A': {'total': 3}, 'B': {'total': 3}}

# Calculate averages with finalize
aggs = {
    "avg": Agg(
        map=lambda x: (x["val"], 1),
        zero=(0, 0),
        reduce=lambda a, b: (a[0] + b[0], a[1] + b[1]),
        finalize=lambda x: x[0] / x[1] if x[1] > 0 else 0
    )
}
result = nest_agg(items, keys=["cat"], aggs=aggs)
# {'A': {'avg': 1.5}, 'B': {'avg': 3.0}}

dictutils.ops - Advanced Operations

The ops module provides 20+ utilities for advanced dictionary manipulation:

Path Operations

from dictutils.ops import deep_get, deep_set, deep_has, ensure_path

d = {"user": {"profile": {"name": "Alice"}}}
deep_get(d, "user.profile.name")    # "Alice"
deep_set(d, "user.profile.age", 30) # Creates nested path if needed
deep_has(d, "user.settings")        # False
ensure_path(d, "user.settings")     # Creates empty dict at path

Data Transformation

from dictutils.ops import flatten_paths, expand_paths, pivot, transpose_dict

# Flatten nested structure to dot notation
flatten_paths({"a": {"b": 1, "c": 2}})  # {"a.b": 1, "a.c": 2}

# Expand dot notation back to nested
expand_paths({"a.b": 1, "a.c": 2})     # {"a": {"b": 1, "c": 2}}

# Transpose nested dict structure
transpose_dict({"A": {"X": 1, "Y": 2}, "B": {"X": 3, "Y": 4}})
# {"X": {"A": 1, "B": 3}, "Y": {"A": 2, "B": 4}}

Aggregation Helpers

from dictutils.ops import group_by, count_by, sum_by, index_by

users = [
    {"id": 1, "name": "Alice", "dept": "eng", "salary": 90000},
    {"id": 2, "name": "Bob", "dept": "eng", "salary": 85000},
    {"id": 3, "name": "Carol", "dept": "sales", "salary": 70000}
]

group_by(users, "dept")                    # {"eng": [...], "sales": [...]}
count_by(users, "dept")                    # {"eng": 2, "sales": 1}  
sum_by(users, "dept", lambda x: x["salary"]) # {"eng": 175000, "sales": 70000}
index_by(users, "id")                      # {1: {...}, 2: {...}, 3: {...}}

Real-World Examples

Sales Data Analysis

from dictutils import qsdict, nest_agg, Agg

# Convert query results to nested structure
sales = [
    {"region": "North", "product": "Widget", "revenue": 1000, "units": 50},
    {"region": "North", "product": "Gadget", "revenue": 1500, "units": 30},
    {"region": "South", "product": "Widget", "revenue": 800, "units": 40},
]

# Group by region -> product, show revenue
by_region = qsdict(sales, "region", "product", "revenue")
# {"North": {"Widget": 1000, "Gadget": 1500}, "South": {"Widget": 800}}

# Calculate metrics with aggregation
aggs = {
    "total_revenue": Agg(map=lambda x: x["revenue"], zero=0),
    "avg_price": Agg(
        map=lambda x: (x["revenue"], x["units"]),
        zero=(0, 0), 
        reduce=lambda a, b: (a[0] + b[0], a[1] + b[1]),
        finalize=lambda x: x[0] / x[1] if x[1] > 0 else 0
    )
}
metrics = nest_agg(sales, keys=["region"], aggs=aggs)
# {"North": {"total_revenue": 2500, "avg_price": 31.25}, ...}

Configuration Management

from dictutils import mergedict
from dictutils.ops import deep_get, deep_set

# Merge multiple config sources
default_config = {"db": {"host": "localhost", "port": 5432}}
user_config = {"db": {"password": "secret"}, "cache": {"ttl": 300}}
env_config = {"db": {"host": "prod-db.example.com"}}

config = mergedict({}, default_config, user_config, env_config)
# Result: {"db": {"host": "prod-db.example.com", "port": 5432, "password": "secret"}, "cache": {"ttl": 300}}

# Access nested values safely
db_host = deep_get(config, "db.host", default="localhost")
deep_set(config, "logging.level", "INFO")  # Creates nested structure

Documentation

📖 Complete Documentation - API reference, cookbook, and examples

Development

  • Install dev tools: pip install .[test,typecheck,lint]
  • Run tests: pytest
  • Type check: mypy dictutils
  • Lint: ruff dictutils
  • Format: black dictutils
  • Upgrade syntax: pyupgrade --py39-plus <file.py>
  • Pre-commit: pre-commit install (then git commit auto-runs checks)

Contributing

Contributions are welcome! See our documentation for development setup and coding standards.

Migration from 0.1.x

This is a major release with breaking changes. See CHANGELOG.md for detailed migration guidance.

License

MIT License. Copyright (c) 2020–2025 Adi Eyal.

Release files for dictutils 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dictutils 1.0.1
File Size Uploaded
dictutils-1.0.1.tar.gz 47.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dictutils 1.0.1
File Interpreter ABI Platform
dictutils-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.4 kB

Release files / dictutils-1.0.1.tar.gz

Download URL dictutils-1.0.1.tar.gz
Size 47.6 kB
Tags Source
SHA-256 checksum
How to use checksums
58f11ddaba13a9951de60e19d46e28cecf0b23127192e93514ff84e810303166
BLAKE2b-256 checksum
How to use checksums
b7ace8874d3bf34c3691f185e23989d49e693e0cc02e711c825a03ed211e2250
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / dictutils-1.0.1-py3-none-any.whl

Download URL dictutils-1.0.1-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
751f410ccf204ec18a36226b48c8efcda4bcd2cb2db3e0a584b874fd9ac0d2f4
BLAKE2b-256 checksum
How to use checksums
a860c6c384e50619953cf162982c0ae933378f674750638e405bd20f01b4575f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

0.1.7

1 release file

0.1.6

1 release file

0.1.5

1 release file

0.1.4

1 release file

0.1.3

1 release file

0.1.2

1 release file

0.1.1

2 release files

0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page