Python Dictionaries on Steroids with Python-benedict

Python Dictionaries on Steroids with Python-benedict

Python-benedict is a powerful Python library that extends the capabilities of Python's built-in dictionary (or dict) class. The library enables you to easily access, search, and modify nested values, manipulate and transform data, and convert various formats to and from dictionaries. As the dictionary is one of the most commonly used data structures in Python, this library could be a potential boost to productivity.

This library is a dict subclass with keylist/keypath support, I/O shortcuts (base64, csv, ini, json, pickle, plist, query-string, toml, xls, xml, yaml) and many utilities... for humans, obviously.

Features

  • 100% backward-compatible, you can safely wrap existing dictionaries.
  • NEW Keyattr support for get/set items using keys as attributes.
  • Keylist support using list of keys as key.
  • Keypath support using keypath-separator (dot syntax by default).
  • Keypath list-index support (also negative) using the standard [n] suffix.
  • Normalized I/O operations with most common formats: base64, csv, ini, json, pickle, plist, query-string, toml, xls, xml, yaml.
  • Multiple I/O operations backends: file-system (read/write), url (read-only), s3 (read/write).
  • Many utility and parse methods to retrieve data as needed (check the API section).
  • Well tested. ;)

Installation

If you want to install everything:

  • pip install "python-benedict[all]"

alternatively you can install the main package:

  • run pip install python-benedict, then install only the optional requirements you need.

Optional Requirements

Here the hierarchy of possible installation targets available when running pip install "python-benedict[...]" (each target installs all its sub-targets):

  • [all]
  • [io]
  • [toml]
  • [xls]
  • [xml]
  • [yaml]
  • [s3]

Usage

Basics

benedict is a dict subclass, so it is possible to use it as a normal dictionary (you can just cast an existing dict).

from benedict import benedict

# create a new empty instance
d = benedict()

# or cast an existing dict
d = benedict(existing_dict)

# or create from data source (filepath, url or data-string) in a supported format:
# Base64, CSV, JSON, TOML, XML, YAML, query-string
d = benedict("https://localhost:8000/data.json", format="json")

# or in a Django view
params = benedict(request.GET.items())
page = params.get_int("page", 1)

Keyattr

It is possible to get/set items using keys as attributes (dotted notation).

from benedict import benedict

d = benedict()
d.profile.firstname = "Fabio"
d.profile.lastname = "Caccamo"
print(d) 
# output:
{'profile': {'firstname': 'Fabio', 'lastname': 'Caccamo'}}

Disable keyattr functionality

You can disable the keyattr functionality passing keyattr_enabled=False in the constructor.

d = benedict(existing_dict, keyattr_enabled=False)

You can disable the keyattr functionality using the getter/setter property.

d.keyattr_enabled = False

Keylist

Wherever a key is used, it is possible to use also a list (or a tuple) of keys.

from benedict import benedict

d = benedict()

# set values by keys list
d["profile", "firstname"] = "Fabio"
d["profile", "lastname"] = "Caccamo"
print(d) 
# output:
{'profile': {'firstname': 'Fabio', 'lastname': 'Caccamo'}}

print(d["profile"]) 
# output:
{'firstname': 'Fabio', 'lastname': 'Caccamo'}

# check if keypath exists in dict
print(["profile", "lastname"] in d) 
# output:
True

# delete value by keys list
del d["profile", "lastname"]
print(d["profile"])
# output:
{'firstname': 'Fabio'}

Keypath

. is the default keypath separator.

Custom keypath separator

You can customize the keypath separator passing the keypath_separator argument in the constructor. If you pass an existing dict to the constructor and its keys contain the keypath separator an Exception will be raised.

d = benedict(existing_dict, keypath_separator="/")

Change keypath separator

You can change the keypath_separator at any time using the getter/setter property. If any existing key contains the new keypath_separator an Exception will be raised.

d.keypath_separator = "/"

Disable keypath functionality

You can disable the keypath functionality passing keypath_separator=None in the constructor.

d = benedict(existing_dict, keypath_separator=None)

You can disable the keypath functionality using the getter/setter property.

d.keypath_separator = None

Examples

from benedict import benedict

data = {
    "id_1": {
        "name": "John",
        "surname": "Doe",
        "age": 25,
        "skills": {
            "programming": {
                "Python": "5",
                "Java": "4",
            },
        },
        "languages": ["English", "French", "Spanish"],
    },
    "id_2": {
        "name": "Bob",
        "surname": "Marley",
        "age": 29,
        "skills": {
            "programming": {
                "Python": "4",
                "JavaScript": "4",
            },
        },
        "languages": ["English", "Portugal", "Italy"],
    }
}

data = benedict(data)


[data[key, "name"] for key in data ]
# output:
['John', 'Bob']

[(data[key, "name"], data[key, "skills"]) for key in data ]
# output:
[('John', {'programming': {'Python': '5', 'Java': '4'}}), ('Bob', {'programming': {'Python': '4', 'JavaScript': '4'}})]



# keypaths
# Return a list of all keypaths in the dict.
# If indexes is True, the output will include list values indexes.
# k = d.keypaths(indexes=False)
data.keypaths()
# output:
['id_1', 'id_1.age', 'id_1.languages', 'id_1.name', 'id_1.skills', 'id_1.skills.programming', 'id_1.skills.programming.Java', 'id_1.skills.programming.Python', 'id_1.surname', 'id_2', 'id_2.age', 'id_2.languages', 'id_2.name', 'id_2.skills', 'id_2.skills.programming', 'id_2.skills.programming.JavaScript', 'id_2.skills.programming.Python', 'id_2.surname']


# filter
# Return a filtered dict using the given predicate function.
# Predicate function receives key, value arguments and should return a bool value.
# predicate = lambda k, v: v is not None
# f = d.filter(predicate)
data.filter(lambda key, value: value.age == 29)
# output:
{'id_2': {'name': 'Bob', 'surname': 'Marley', 'age': 29, 'skills': {'programming': {'Python': '4', 'JavaScript': '4'}}, 'languages': ['English', 'Portugal', 'Italy']}}
data.filter(lambda key, value: "Java" in value.skills.programming)
# output:
{'id_1': {'name': 'John', 'surname': 'Doe', 'age': 25, 'skills': {'programming': {'Python': '5', 'Java': '4'}}, 'languages': ['English', 'French', 'Spanish']}}


# flatten
# Return a new flattened dict using the given separator to join nested dict keys to flatten keypaths.
# f = d.flatten(separator="_")
print(data.flatten(separator="/").dump())
# output:
{
    "id_1/age": 25,
    "id_1/languages": [
        "English",
        "French",
        "Spanish"
    ],
    "id_1/name": "John",
    "id_1/skills/programming/Java": "4",
    "id_1/skills/programming/Python": "5",
    "id_1/surname": "Doe",
    "id_2/age": 29,
    "id_2/languages": [
        "English",
        "Portugal",
        "Italy"
    ],
    "id_2/name": "Bob",
    "id_2/skills/programming/JavaScript": "4",
    "id_2/skills/programming/Python": "4",
    "id_2/surname": "Marley"
}


# search
# Search and return a list of items (dict, key, value, ) matching the given query.
# r = d.search("hello", in_keys=True, in_values=True, exact=False, case_sensitive=False)
data.search("Java", in_keys=True, in_values=True, exact=False, case_sensitive=False)
# output:
[({'Python': '5', 'Java': '4'}, 'Java', '4'), ({'Python': '4', 'JavaScript': '4'}, 'JavaScript', '4')]
data.search("Java", in_keys=True, in_values=True, exact=True, case_sensitive=False)
# output:
[({'Python': '5', 'Java': '4'}, 'Java', '4')]


# subset
# Return a dict subset for the given keys.
# It is possible to pass a single key or more keys (as list or *args).
# s = d.subset(["firstname", "lastname", "email"])
data["id_2"].subset(["skills"])
# output:
{'skills': {'programming': {'Python': '4', 'JavaScript': '4'}}}
data.id_2.subset(["skills"])
# output:
{'skills': {'programming': {'Python': '4', 'JavaScript': '4'}}}

SUBSCRIBE FOR NEW ARTICLES

@
comments powered by Disqus