Getting started

Simply import easytree and create nested nodes on the fly using the dot notation.

>>> import easytree

>>> tree = easytree.dict()
>>> tree.foo.bar.baz = "Hello world!"
>>> tree
{
    "foo": {
        "bar": {
            "baz": "Hello world!"
        }
    }
}

Inheritence

easytree.dict inherits from the native python dict class.

>>> tree = easytree.dict({"foo":"bar"})
>>> isinstance(tree, dict)
True

easytree.list also inherits from the native python list class.

>>> numbers = easytree.list([1,3,5])
>>> isinstance(numbers, list)
True

Setting or assigning values

Instead of raising an AttributeError, reading a new attribute on an easytree.dict returns a new undefined node.

>>> tree = easytree.dict()
>>> tree.address # undefined node
<undefined 'address'>

Reading or setting an attribute on such child node dynamically casts it as an easytree.dict and assigns it to its parent.

>>> tree.address.country = "United States"
>>> tree.address # now a dict
{"country": "United States"}

Alternatively, using a list method such as append dynamically casts the new node as an easytree.list

>>> tree = easytree.dict()
>>> tree.address # undefined node
<undefined 'address'>

>>> tree.address.country.append("United States")
>>> tree.address # now a dict
{"country": ["United States"]}

Note

Technically, casting replaces the node with an appropriate class instance (e.g. dict or list) in the parent object

Of course, you can use the dot or bracket notation interchangeably, both to read and assign nested values

>>> tree = easytree.dict({"foo":"bar"})
>>> tree["foo"]
"bar"
>>> tree.foo
"bar"

Note

The bracket notation remains necessary if the key is not a valid attribute identifier name, or if the key is identical to a dict method name.

>>> tree = easytree.dict()
>>> tree["attribute with space"] = True
>>> tree
{"attribute with space": True}

Nested assignment

Dictionaries assigned to an easytree.dict or added to an easytree.list are themselves cast as easytree.dict instances, allowing you to use the dot notation on nested dictionaries.

>>> friends = easytree.list()
>>> friends.append({"firstname": "Alice"})
>>> isinstance(friends[0], easytree.dict)
True
>>> friends[0].address.country = "Netherlands"
>>> friends
[
    {
        "firstname": "Alice",
        "address": {
            "country": "Netherlands"
        }
    }
]

Lists assigned to an easytree.dict are also cast as easytree.list instances.

>>> tree = easytree.dict({"numbers": [1,3,5]})
>>> isinstance(tree.numbers, easytree.list)
True

Tuple values assigned to an easytree.dict are also cast as tuples of easytree objects.

>>> tree = easytree.dict({"country": ("France", {"capital": "Paris"})})
>>> isinstance(tree.country, tuple)
True
>>> tree.country[0]
'France'
>>> tree.country[0].capital
'Paris'

Getter

The get method of easytree.dict is supercharged to query deeply-nested trees.

>>> profile = easytree.dict()
>>> profile.friends.append({"name":"Bob", "address":{"country":"France"}})
>>> profile.get(["friends", 0, "address", "country"])
France
>>> profile.get(["friends", 0, "address", "street"])
None

Hint

Normally, this would raise an error, as a list is not hashable. This means no collisions are possible between keys and such list queries.

Context manager

The context manager returns the node, such that writing deeply-nested trees is easier:

>>> order = easytree.dict()
>>> with order.customer.delivery.address as a:
...     a.country = "United States"
...     a.city    = "New York"
...     a.street  = "5th avenue"
>>> order
{
    "order": {
        "customer": {
            "delivery": {
                "address": {
                    "country": "United States",
                    "city": "New York",
                    "street": "5th avenue"
                }
            }
        }
    }
}

Because the append method returns a reference to the last appended item, writing deeply-nested trees which combine easytree.dict and easytree.list nodes is also easy:

>>> profile = easytree.dict()
>>> with profile.friends.append({"firstname":"Flora"}) as friend:
...     friend.birthday = "25/02"
...     friend.address.country = "France"
>>> profile
{
    "friends": [
        {
            "firstname": "Flora",
            "birthday": "25/02",
            "address": {
                "country": "France"
            }
        }
    ]
}

The undefined node

An undefined node object is returned when an undefined attribute is read from an easytree.dict node. This falsy object contains a reference to its parent object, as well as the key from which this object was returned.

>>> person = easytree.dict()
>>> person.address
<undefined 'address'>

Assigning or reading an attribute from an undefined node casts it as a dictionary. This is possible since the undefined object keeps a reference to its parent and the key from which it was returned.

>>> person = easytree.dict()
>>> person.address.country = "Nigeria"
>>> person.address
{"country": "Nigeria"}

Using the bracket notation works identically.

>>> person = easytree.dict()
>>> person["address"].country = "Nigeria"
>>> person.address
{"country": "Nigeria"}

An undefined node evaluates to False.

>>> person = easytree.dict()
>>> if not person.address:
...     print("address is missing")
address is missing

Pitfalls

By definition, and unless an easytree is sealed or frozen, reading an undefined attribute will not raise an exception.

>>> profile = easytree.dict({"firstname":"David"})
>>> profile.firstnam #typo
<undefined 'firstnam'>

Using a numeric key on an undefined node will cast the node as a dictionary, not a list.

>>> profile = easytree.dict({"firstname":"David"})
>>> profile.friends[0].name = "Flora"
>>> profile
{
    "friends": {
        0: "Flora"
    }
}

Dictionary and lists added to an easytree are cast to an easytree.dict or easytree.list instance. This means identity is not preserved.

>>> point = {"x":1, "y":1}
>>> graph = easytree.list([point])
>>> point in graph
True
>>> graph[0] is point
False