Skip to main content

Small utilities for nested dicts: merge, pivot, grouping/aggregation.

Project description

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.

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

dictutils-1.0.1.tar.gz (47.6 kB view details)

Uploaded Source

Built Distribution

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

dictutils-1.0.1-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

Details for the file dictutils-1.0.1.tar.gz.

File metadata

  • Download URL: dictutils-1.0.1.tar.gz
  • Upload date:
  • Size: 47.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for dictutils-1.0.1.tar.gz
Algorithm Hash digest
SHA256 58f11ddaba13a9951de60e19d46e28cecf0b23127192e93514ff84e810303166
MD5 217e1bfd2fe3dbd545f9583c0b66b234
BLAKE2b-256 b7ace8874d3bf34c3691f185e23989d49e693e0cc02e711c825a03ed211e2250

See more details on using hashes here.

File details

Details for the file dictutils-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: dictutils-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 21.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for dictutils-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 751f410ccf204ec18a36226b48c8efcda4bcd2cb2db3e0a584b874fd9ac0d2f4
MD5 0bc9a18f90c724ebaef12edb86e3af57
BLAKE2b-256 a860c6c384e50619953cf162982c0ae933378f674750638e405bd20f01b4575f

See more details on using hashes here.

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