Python, handling dictionaries with dot notation

Introduction

A simple JSON file :

resources.json
{
	"id": 307
	,"date": "April 14, 2020"
	,"thumbnail": "python"
	,"langurls":[
		{"lang":"fr" , "file":"python-parsing-des-arguments-argparse-et-getopt.html"}
		,{"lang":"en", "file":"python-parsing-arguments-packages-argparse-getopt.html"}
	]
  ,"sitemap" : {  "title" : "Conception - Python", "url" : "/conception/python" }
}

With Javascript, after fetching a JSON file, properties or attributes are usually handled in dot notation :

props = {};

load_prop = async function() {
	const res = await fetch("https://www.sqlpac.ger/referentiel/docs/resources.json");
	if (res.status==200)  response = await res.json(); 
	props = response;
};

show_prop = function() {
	console.log(props.date);
};

load_prop().then(show_prop);
console> April 14, 2020

Switching to Python, bad news : dot notation is not directly available with the resulting dictionary.

import json

def main():
  with open('resources.json','rb') as f:
    props = json.load(f)
    print(props.date)

if (__name__=="__main__"):
  main()
    print(props.date)
AttributeError: 'dict' object has no attribute 'date'

We must use the usual syntax : props["attribute1"]["attribute2"]…

import json

def main():
  with open('resources.json','rb') as f:
    props = json.load(f)
    print(props["date"])

if (__name__=="__main__"):
  main()
April 14, 2020

Python is not Javascript. Habits, habits, hard to remove them. This example deals with loading JSON data, the same context occurs when loading YAML data…

How to use dot notation with Python for dictionaries ? It can be achieved natively but when dictionaries are nested, it becomes tedious without developing its own library. Maintained community packages exist to do the job : Prodict, python-box.

The native methods

Populating a dictionary class

A dictionary class is populated with the data :

import json

class clsprops:
	def __init__(self, data):
		self.__dict__ = data
	
def main():
  with open('resources.json','rb') as f:
    props = clsprops(json.load(f))
    print(props.date)

if (__name__=="__main__"):
  main()
April 14, 2020

But what about nested dictionaries attributes ( { … ,"sitemap": { "url":"…", "title":"…" } … } ) ? Predictable, nested dictionaries attributes can not be used with dot notation

def main():
  with open('resources.json','rb') as f:
    props = clsprops(json.load(f))
    print(props.sitemap.url)
    print(props.sitemap.url)
AttributeError: 'dict' object has no attribute 'url'

props.sitemap["url"] must be used.

Using SimpleNamespace

A more elegant code using SimpleNamespace without declaring a custom class :

import json
from types import SimpleNamespace

def main():
	with open('resources.json','rb') as f: 
		props = SimpleNamespace(** json.load(f))
		print(props.date)

if (__name__=="__main__"):
	main()
April 14, 2020

Same issue than using a custom class, nested dictionaries attributes are not accessible in dot notation :


def main():
	with open('resources.json','rb') as f: 
	props = SimpleNamespace(** json.load(f))
	print(props.sitemap.url)
AttributeError: 'dict' object has no attribute 'url'

Dot notation dictionary packages

Fortunately, there are so many packages in the Python community, so we should find some packages covering this need. The hardest is to find the ones providing the best functionalities, the less dependencies and above all a maintained package (look at the latest releases dates, revealing the package durability and support).

2 packages selected :

prodict

Prodict installation has no dependencies :

pip3 search prodict
prodict (0.8.3)  - Prodict = Pro Dictionary with IDE friendly(auto code completion), dot-accessible attributes and more.
pip3 install product
Successfully installed prodict-0.8.3

Dot notation usage is then very easy, undefined keys returns None instead of the default dict error AttributeError and keys can be dynamically added :

import json
from prodict import Prodict

def main():
	with open('resources.json','rb') as f: 
		props = Prodict.from_dict(json.load(f))
		print(props.sitemap.url)
    
    if (props.sitemap.url2==None):
      props.sitemep.url2="/conception/python-3.8"
      print(props.sitemep.url2)

if (__name__=="__main__"):
	main()
/conception/python
/conception/python-3.8

Just one inconvenient, when nested dictionaries are defined in a list and only in this case :

"langurls":[
		{"lang":"fr" , "file":"python-parsing-des-arguments-argparse-et-getopt.html"}
		,{"lang":"en", "file":"python-parsing-arguments-packages-argparse-getopt.html"}
]

a new Prodict object is necessary to use dot notation, otherwise the error AttributeError is raised :

import json
from prodict import Prodict

def main():
	with open('resources.json','rb') as f: 
    props = Prodict.from_dict(json.load(f))
		
    # print(props.langurls[0].lang)  Not possible with Prodict, Attribute Error raised
    print(Prodict.from_dict(props.langurls[0]).lang)

if (__name__=="__main__"):
	main()
fr

Prodict object is a dictionary, so it can be used as a dictionary and compared to regular dictionaries, no translation is necessary :

		with open('results.json','w') as r:
			json.dump(props, r, indent=4, ensure_ascii=False)
…, 
"sitemap": {
        "title": "Conception - Python",
        "url": "/conception/python",
        "url2": "/conception/python-3.8"
}

python-box

Python-box has dependencies (ruamel.yaml and toml parsers packages) :

pip3 search python-box
python-box (4.2.2)                       - Advanced Python dictionaries with dot notation access
pip3 install python-box
Successfully installed python-box-4.2.2 ruamel.yaml-0.16.10 ruamel.yaml.clib-0.2.0 toml-0.10.0

To use python-box

import json

from box import Box

def main():
	with open('resources.json','rb') as f: 
		props = Box(json.load(f))
		print(props.sitemap.url)

if (__name__=="__main__"):
	main()
/conception/python

About the inconvenient noticed with Prodict, dictionaries defined in a list are well managed by python-box :

import json

from box import Box

def main():
	with open('resources.json','rb') as f: 
		props = Box(json.load(f))
		print(props.sitemap.url)
		print(props.langurls[0].lang)

if (__name__=="__main__"):
	main()

/conception/python
fr

Box objects can be compared to native dictionaries. The method to_dict translates a Box object to a native dictionary, but some tests performed on usual actions (json.dump for example) did not need to use the method to_dict.

The inconvenient :

  • Querying undefined keys raises an error (BoxKeyError)

Conclusion

In the author’s opinion of this paper, Prodict package is the most suitable : no dependencies, very light and above all, we can manage non existing keys as we would do in Javascript without any exception raised, that’s exactly the additional feature we were looking for in addition to the dot notation :

if (props.sitemap.url2 == undefined) { // Javascript code }
if (props.sitemap.url2 == None) :
  # Python code

In the case where dictionaries are defined in a list, workarounds can be easily found.