dictutils
A collection of utilities for manipulating nested dictionaries and data structures in Python.
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
- Quickstart Guide - Get up and running quickly
- API Reference - Complete function documentation
- Cookbook - Real-world examples and patterns
- Advanced Operations - dictutils.ops module guide
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(thengit commitauto-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.
Metadata
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)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| dictutils-1.0.1.tar.gz | 47.6 kB | Without provenance | Details for dictutils-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| dictutils-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for dictutils-1.0.1-py3-none-any.whl |
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
|
Security for dictutils 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.