BuildStatus CoverageStatus License PyPi DocStatus

BoxImage

Python dictionaries with advanced dot notation access.

from box import Box

movie_box = Box({
    "Robin Hood: Men in Tights": {
        "imdb_stars": 6.7,
        "length": 104,
        "stars": [ {"name": "Cary Elwes", "imdb": "nm0000144", "role": "Robin Hood"},
                   {"name": "Richard Lewis", "imdb": "nm0507659", "role": "Prince John"} ]
    }
})

movie_box.Robin_Hood_Men_in_Tights.imdb_stars
# 6.7

movie_box.Robin_Hood_Men_in_Tights.stars[0].name
# 'Cary Elwes'

Box will automatically make otherwise inaccessible keys (“Robin Hood: Men in Tights”) safe to access as an attribute. You can always pass conversion_box=False to Box to disable that behavior.

Also, all new dict and lists added to a Box or BoxList object are converted automatically.

movie_box.Robin_Hood_Men_in_Tights.stars.append(
     {"name": "Roger Rees", "imdb": "nm0715953", "role": "Sheriff of Rottingham"})

movie_box.Robin_Hood_Men_in_Tights.stars[-1].role
# 'Sheriff of Rottingham'

Install

pip install --upgrade python-box

Box 4 is tested on python 3.6+

If you have any issues please open a github issue with the error you are experiencing!

Overview

Box is designed to be an easy drop in transparently replacements for dictionaries, thanks to Python’s duck typing capabilities, which adds dot notation access. Any sub dictionaries or ones set after initiation will be automatically converted to a Box object. You can always run .to_dict() on it to return the object and all sub objects back into a regular dictionary.

movie_box.movies.Spaceballs.to_dict()
{'director': 'Mel Brooks',
 'imdb stars': 7.1,
 'length': 96,
 'personal thoughts': 'On second thought, it was hilarious!',
 'rating': 'PG',
 'stars': [{'imdb': 'nm0000316', 'name': 'Mel Brooks', 'role': 'President Skroob'},
           {'imdb': 'nm0001006', 'name': 'John Candy', 'role': 'Barf'},
           {'imdb': 'nm0001548', 'name': 'Rick Moranis', 'role': 'Dark Helmet'},
           {'imdb': 'nm0000597', 'name': 'Bill Pullman', 'role': 'Lone Starr'}]}

Boxes

Box can be instantiated the same ways as dict.

Box({'data': 2, 'count': 5})
Box(data=2, count=5)
Box({'data': 2, 'count': 1}, count=5)
Box([('data', 2), ('count', 5)])

# All will create
# <Box: {'data': 2, 'count': 5}>

Box is a subclass of dict which overrides some base functionality to make sure everything stored in the dict can be accessed as an attribute or key value.

small_box = Box({'data': 2, 'count': 5})
small_box.data == small_box['data'] == getattr(small_box, 'data')

All dicts (and lists) added to a Box will be converted on lookup to a Box (or BoxList), allowing for recursive dot notation access.

Box also includes helper functions to transform it back into a dict, as well as into JSON or YAML strings or files.

Limitations

Box is a subclass of dict and as such, certain keys cannot be accessed via dot notation. This is because names such as keys and pop have already been declared as methods, so Box cannot use it’s special sauce to overwrite them. However it is still possible to have items with those names in the Box and access them like a normal dictionary, such as my_box[‘keys’].

This is as designed, and will not be changed.

Common non-magic methods that exist in a Box are: clear, copy, from_json, fromkeys, get, items, keys, pop, popitem, setdefault, to_dict, to_json, update, merge_update, values. To view an entire list of what cannot be accessed via dot notation, run the command dir(Box()).

Box’s parameters

Keyword Argument Default Description
conversion_box True Automagically make keys with spaces attribute accessible
frozen_box False Make the box immutable, hashable (if all items are non-mutable)
default_box False Act like a recursive default dict
default_box_attr Box Can overwrite with a different (non-recursive) default attribute to return
camel_killer_box False CamelCaseKeys become attribute accessible like snake case (camel_case_keys)
box_safe_prefix “x” Character or prefix to prepend to otherwise invalid attributes
box_duplicates “ignore” When conversion duplicates are spotted, either ignore, warn or error
box_intact_types () Tuple of objects to preserve and not convert to a Box object
box_recast None cast certain keys to a specified type
box_dots False Allow access to nested dicts by dots in key names

Box’s functions

Function Name Description
to_dict Recursively transform all Box (and BoxList) objects back into a dict (and lists)
to_json Save Box object as a JSON string or write to a file with the filename parameter
to_yaml Save Box object as a YAML string or write to a file with the filename parameter
to_toml* Save Box object as a TOML string or write to a file with the filename parameter
from_json Classmethod, Create a Box object from a JSON file or string (all Box parameters can be passed)
from_yaml Classmethod, Create a Box object from a YAML file or string (all Box parameters can be passed)
from_toml* Classmethod, Create a Box object from a TOML file or string (all Box parameters can be passed)
merge_update Recursively merge dictionaries or Boxes together instead of replacing

* Do not work with BoxList, only Box

Conversion Box

By default, Box is now a conversion_box that adds automagic attribute access for keys that could not normally be attributes. It can of course be disabled with the keyword argument conversion_box=False.

movie_box.movies.Spaceballs["personal thoughts"] = "It was a good laugh"
movie_box.movies.Spaceballs.personal_thoughts
# 'It was a good laugh'

movie_box.movies.Spaceballs.personal_thoughts = "On second thought, it was hilarious!"
movie_box.movies.Spaceballs["personal thoughts"]
# 'On second thought, it was hilarious!'

# If a safe attribute matches a key exists, it will not create a new key
movie_box.movies.Spaceballs["personal_thoughts"]
# KeyError: 'personal_thoughts'

Keys are modified in the following steps to make sure they are attribute safe:

  1. Convert to string (Will encode as UTF-8 with errors ignored)
  2. Replaces any spaces with underscores
  3. Remove anything other than ascii letters, numbers or underscores
  4. If the first character is an integer, it will prepend a lowercase ‘x’ to it
  5. If the string is a built-in that cannot be used, it will prepend a lowercase ‘x’
  6. Removes any duplicate underscores

This does not change the case of any of the keys.

bx = Box({"321 Is a terrible Key!": "yes, really"})
bx.x321_Is_a_terrible_Key
# 'yes, really'

These keys are not stored anywhere, and trying to modify them as an attribute will actually modify the underlying regular key’s value.

Warning: duplicate attributes possible

If you have two keys that evaluate to the same attribute, such as “a!b” and “a?b” would become .ab, there is no way to discern between them, only reference or update them via standard dictionary modification.

Frozen Box

Want to show off your box without worrying about others messing it up? Freeze it!

frigid = Box(data={'Python': 'Rocks', 'inferior': ['java', 'cobol']}, frozen_box=True)

frigid.data.Python = "Stinks"
# box.BoxError: Box is frozen

frigid.data.Python
# 'Rocks'

hash(frigid)
# 4021666719083772260

frigid.data.inferior
# ('java', 'cobol')

It’s hashing ability is the same as the humble tuple, it will not be hashable if it has mutable objects. Speaking of tuple, that’s what all the lists becomes now.

Default Box

It’s boxes all the way down. At least, when you specify default_box=True it can be.

empty_box = Box(default_box=True)

empty_box.a.b.c.d.e.f.g
# <Box: {}>

# BOX 4.1 change, on lookup the sub boxes are created
print(empty_box)
# <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': {}}}}}}}}>

empty_box.a.b.c.d.e.f.g = "h"
empty_box
# <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': 'h'}}}}}}}>

Unless you want it to be something else.

evil_box = Box(default_box=True, default_box_attr="Something Something Something Dark Side")

evil_box.not_defined
# 'Something Something Something Dark Side'

# Keep in mind it will no longer be possible to go down multiple levels
evil_box.not_defined.something_else
# AttributeError: 'str' object has no attribute 'something_else'

default_box_attr will first check if it is callable, and will call the object if it is, otherwise it will see if has the copy attribute and will call that, lastly, will just use the provided item as is.

4.1 Update: Previous versions had an error when something that evaluated to None would also return a box, such as an empty string or empty list. This behavior has been fixed.

Camel Killer Box

Similar to how conversion box works, allow CamelCaseKeys to be found as snake_case_attributes.

cameled = Box(BadHabit="I just can't stop!", camel_killer_box=True)

cameled.bad_habit
# "I just can't stop!"

Box Recast Values

Automatically convert all incoming values of a particular key (at root or any sub box) to a different type.

For example, if you wanted to make sure any field labeled ‘id’ was an integer:

my_box = Box(box_recast={'id': int})

my_box.new_key = {'id': '55', 'example': 'value'}

print(type(my_box.new_key.id))
# 55

If it cannot be converted, it will raise a BoxValueError (catachable with either BoxError or ValueError as well)

my_box = Box(box_recast={'id': int})

my_box.id = 'Harry'

# box.exceptions.BoxValueError: Cannot convert Harry to <class 'int'>

Box Intact Types

Do you not want box to convert lists or tuples or incoming dictionaries for some reasonn? That’s totally fine, we got you covered!

my_box = Box(box_intact_types=[list, tuple])

# Don't automatically convert lists into #BoxList
my_box.new_data = [{'test': 'data'}]

print(type(my_box.new_data))
# <class 'list'>

Box Dots

A new way to traverse the Box!

my_box = Box(box_dots=True)

my_box.incoming = {'new': {'source 1': {'$$$': 'money'}}}

print(my_box['incoming.new.source 1.$$$'])
# money

my_box['incoming.new.source 1.$$$'] = 'spent'
print(my_box)
# {'incoming': {'new': {'source 1': {'$$$': 'spent'}}}}

Be aware, if those sub boxes didn’t exist as planned, a new key with that value would be created instead

del my_box['incoming']
my_box['incoming.new.source 1.$$$'] = 'test'
print(my_box)

# {'incoming.new.source 1.$$$': 'test'}

4.1 Update: Support for traversing box lists as well!

my_box = Box({'data': [ {'rabbit': 'hole'} ] }, box_dots=True)
print(data.data[0].rabbit)
# hole

This does only work for keys that are already strings as of version 4.1.

BoxList

To make sure all items added to lists in the box are also converted, all lists are covered into BoxList. It’s possible to initiate these directly and use them just like a Box.

from box import BoxList

my_boxlist = BoxList({'item': x} for x in range(10))
#  <BoxList: [<Box: {'item': 0}>, <Box: {'item': 1}>, ...

my_boxlist[5].item
# 5

to_list

Transform a BoxList and all components back into regular list and dict items.

my_boxlist.to_list()
# [{'item': 0},
#  {'item': 1},
#  ...

SBox

Shorthand Box, aka SBox for short(hand), has the properties json, yaml and dict for faster access than the regular to_dict() and so on.

from box import SBox

sb = SBox(test=True)
sb.json
# '{"test": true}'

Note that in this case, json has no default indent, unlike to_json.

ConfigBox

A Box with additional handling of string manipulation generally found in config files.

test_config.ini

[General]
example=A regular string

[Examples]
my_bool=yes
anint=234
exampleList=234,123,234,543
floatly=4.4

With the combination of reusables and ConfigBox you can easily read python config values into python types. It supports list, bool, int and float.

import reusables
from box import ConfigBox

config = ConfigBox(reusables.config_dict("test_config.ini"))
# <ConfigBox: {'General': {'example': 'A regular string'},
# 'Examples': {'my_bool': 'yes', 'anint': '234', 'examplelist': '234,123,234,543', 'floatly': '4.4'}}>

config.Examples.list('examplelist')
# ['234', '123', '234', '543']

config.Examples.float('floatly')
# 4.4

Thoughts

“Awesome time (and finger!) saver.” - Zenlc2000

“no thanks.” - burnbabyburn

“I just prefer plain dictionaries.” - falcolas

Thanks

A huge thank you to everyone that has given features and feedback over the years to Box!

Check out everyone that has contributed.

A special thanks to Python Software Foundation, and PSF-Trademarks Committee, for official approval to use the Python logo on the Box logo!

Also special shout-out to PythonBytes, who featured Box on their podcast.

History

Feb 2014: Inception

Box was first created under the name Namespace in the reusables package. Years of usage and suggestions helped mold it into the largest section of the reusables library.

Mar 2017: Box 1.0

After years of upgrades it became clear it was used more than most other parts of the reusables library of tools. Box become its own package.

Mar 2017: BoxLists

2.0 quickly followed 1.0, adding BoxList to allow for further dot notations while down in lists. Also added the handy to_json and to_yaml functionality.

May 2017: Options

Box 3.0 brought a lot of options to the table for maximum customization. From allowing you to freeze the box or just help you find your attributes when accessing them by dot notation.

Dec 2019: 2.7 EOL

Box 4.0 was made with python 2.x out of mind. Everything from f-strings to type-hinting was added to update the package. The modules grew large enough to separate the different objects into their own files and test files.

License

MIT License, Copyright (c) 2017-2020 Chris Griffith. See LICENSE file.

github.com/cdgriffith/Reusables/commit/df20de4db74371c2fedf1578096f3e29c93ccdf3#diff-e9a0f470ef3e8afb4384dc2824943048R51

Changelog

Version 4.2.2

  • Fixing default_box doesn’t first look for safe attributes before falling back to default (thanks to Pymancer)
  • Changing from TravisCI to Github Actions

Version 4.2.1

  • Fixing uncaught print statement (thanks to Bruno Rocha)
  • Fixing old references to box_it_up in the documentation

Version 4.2.0

  • Adding optimizations for speed ups to creation and inserts
  • Adding internal record of safe attributes for faster lookups, increases memory footprint for speed (thanks to Jonas Irgens Kylling)
  • Adding all additional methods specific to Box as protected keys
  • Fixing merge_update from incorrectly calling __setattr__ which was causing a huge slowdown (thanks to Jonas Irgens Kylling)
  • Fixing copy and __copy__ not copying box options

Version 4.1.0

  • Adding support for list traversal with box_dots (thanks to Lei)
  • Adding BoxWarning class to allow for the clean suppression of warnings
  • Fixing default_box_attr to accept items that evaluate to None (thanks to Wenbo Zhao and Yordan Ivanov)
  • Fixing BoxList to properly send internal box options down into new lists
  • Fixing issues with conversion and camel killer boxes not being set properly on insert
  • Changing default_box to set objects in box on lookup
  • Changing camel_killer to convert items on insert, which will change the keys when converted back to dict unlike before
  • Fallback to PyYAML if ruamel.yaml is not detected (thanks to wim glenn)
  • Removing official support for pypy as it’s pickling behavior is not the same as CPython
  • Removing internal __box_heritage as it was no longer needed due to behavior update

Version 4.0.4

  • Fixing get to return None when not using default box (thanks to Jeremiah Lowin)

Version 4.0.3

  • Fixing non-string keys breaking when box_dots is enabled (thanks to Marcelo Huerta)

Version 4.0.2

  • Fixing converters to properly pass through new box arguments (thanks to Marcelo Huerta)

Version 4.0.1

  • Fixing setup.py for release
  • Fixing documentation link

Version 4.0.0

  • Adding support for retrieving items via dot notation in keys
  • Adding box_from_file helper function
  • Adding merge_update that acts like previous Box magic update
  • Adding support to + boxes together
  • Adding default_box now can support expanding on None placeholders (thanks to Harun Tuncay and Jeremiah Lowin)
  • Adding ability to recast specified fields (thanks to Steven McGrath)
  • Adding to_csv and from_csv capability for BoxList objects (thanks to Jiuli Gao)
  • Changing layout of project to be more object specific
  • Changing update to act like normal dict update
  • Changing to 120 line character limit
  • Changing how safe_attr handles unsafe characters
  • Changing all exceptions to be bases of BoxError so can always be caught with that base exception
  • Changing delete to also access converted keys (thanks to iordanivanov)
  • Changing from PyYAML to ruamel.yaml as default yaml import, aka yaml version default is 1.2 instead of 1.1
  • Removing ordered_box as Python 3.6+ is ordered by default
  • Removing BoxObject in favor of it being another module

Version 3.4.6

  • Fixing allowing frozen boxes to be deep copyable (thanks to jandelgado)

Version 3.4.5

  • Fixing update does not convert new sub dictionaries or lists (thanks to Michael Stella)
  • Changing update to work as it used to with sub merging until major release

Version 3.4.4

  • Fixing pop not properly resetting box_heritage (thanks to Jeremiah Lowin)

Version 3.4.3

  • Fixing propagation of box options when adding a new list via setdefault (thanks to Stretch)
  • Fixing update does not keep box_intact_types (thanks to pwwang)
  • Fixing update to operate the same way as a normal dictionary (thanks to Craig Quiter)
  • Fixing deepcopy not copying box options (thanks to Nikolay Stanishev)

Version 3.4.2

  • Adding license, changes and authors files to source distribution

Version 3.4.1

  • Fixing copy of inherited classes (thanks to pwwang)
  • Fixing get when used with default_box

Version 3.4.0

  • Adding box_intact_types that allows preservation of selected object types (thanks to pwwang)
  • Adding limitations section to readme

Version 3.3.0

  • Adding BoxObject (thanks to Brandon Gomes)

Version 3.2.4

  • Fixing recursion issue #68 when using setdefault (thanks to sdementen)
  • Fixing ordered_box would make ‘ordered_box_values’ internal helper as key in sub boxes

Version 3.2.3

  • Fixing pickling with default box (thanks to sdementen)

Version 3.2.2

  • Adding hash abilities to new frozen BoxList
  • Fixing hashing returned unpredictable values (thanks to cebaa)
  • Fixing update to not handle protected words correctly (thanks to deluxghost)
  • Removing non-collection support for mapping and callable identification

Version 3.2.1

  • Fixing pickling on python 3.7 (thanks to Martijn Pieters)
  • Fixing rumel loader error (thanks to richieadler)
  • Fixing frozen_box does not freeze the outermost BoxList (thanks to V.Anh Tran)

Version 3.2.0

  • Adding ordered_box option to keep key order based on insertion (thanks to pwwang)
  • Adding custom __iter__, __revered__, pop, popitems
  • Fixing ordering of camel_case_killer vs default_box (thanks to Matan Rosenberg)
  • Fixing non string keys not being supported correctly (thanks to Matt Wisniewski)

Version 3.1.1

  • Fixing __contains__ (thanks to Jiang Chen)
  • Fixing get could return non box objects

Version 3.1.0

  • Adding copy and deepcopy support that with return a Box object
  • Adding support for customizable safe attr replacement
  • Adding custom error for missing keys
  • Changing that for this 3.x release, 2.6 support exists
  • Fixing that a recursion loop could occur if _box_config was somehow removed
  • Fixing pickling

Version 3.0.1

  • Fixing first level recursion errors
  • Fixing spelling mistakes (thanks to John Benediktsson)
  • Fixing that list insert of lists did not use the original list but create an empty one

Version 3.0.0

  • Adding default object abilities with default_box and default_box_attr kwargs
  • Adding from_json and from_yaml functions to both Box and BoxList
  • Adding frozen_box option
  • Adding BoxError exception for custom errors
  • Adding conversion_box to automatically try to find matching attributes
  • Adding camel_killer_box that converts CamelCaseKeys to camel_case_keys
  • Adding SBox that has json and yaml properties that map to default to_json() and to_yaml()
  • Adding box_it_up property that will make sure all boxes are created and populated like previous version
  • Adding modify_tuples_box option to recreate tuples with Boxes instead of dicts
  • Adding to_json and to_yaml for BoxList
  • Changing how the Box object works, to conversion on extraction
  • Removing __call__ for compatibly with django and to make more like dict object
  • Removing support for python 2.6
  • Removing LightBox
  • Removing default indent for to_json

Version 2.2.0

  • Adding support for ruamel.yaml (Thanks to Alexandre Decan)
  • Adding Contributing and Authors files

Version 2.1.0

  • Adding .update and .set_default functionality
  • Adding dir support

Version 2.0.0

  • Adding BoxList to allow for any Box to be recursively added to lists as well
  • Adding to_json and to_yaml functions
  • Changing Box original functionality to LightBox, Box now searches lists
  • Changing Box callable to return keys, not values, and they are sorted
  • Removing tree_view as near same can be seen with YAML

Version 1.0.0

  • Initial release, copy from reusables.Namespace
  • Original creation, 2132014

Box 4.0 Features and Changes

Box 4.0 has brought a lot of great new features, but also some breaking changes. They are documented here to help you upgrade.

To install the latest 4.x you will need at least Python 3.6 (or current supported python 3.x version)

..code:: bash

pip install –upgrade python-box>=4

If your application is no longer working, and need a quick fix:

..code:: bash

pip install –upgrade python-box<4

Additions

Dot notation access by keys!

Enabled with box_dots=True.

from box import Box
my_box = Box(a={'b': {'c': {'d': 'my_value'}}}, box_dots=True)
print(my_box['a.b.c.d'])
# 'my_value'
my_box['a.b.c.d'] = 'test'
# <Box: {'a': {'b': {'c': {'d': 'test'}}}}>
del my_box['a.b.c.d']
# <Box: {'a': {'b': {'c': {}}}}>

This only works with keys that are string to begin with, as we don’t do any automatic conversion behind the scene.

4.1 Update: This now also supports list traversal, like my_box[‘my_key[0][0]’]

Support for adding two Boxes together

from box import Box
Box(a=4) + Box(a='overwritten', b=5)
# <Box: {'a': 'overwritten', 'b': 5}>

Additional additions

  • Added toml conversion support
  • Added CSV conversion support
  • Added box_from_file helper function

Changes

Adding merge_update as its own function, update now works like the default dictionary update

Traditional update is destructive to nested dictionaries.

from box import Box
box_one = Box(inside_dict={'data': 5})
box_two = Box(inside_dict={'folly': True})
box_one.update(box_two)
repr(box_one)
# <Box: {'inside_dict': {'folly': True}}>

Merge update takes existing sub dictionaries into consideration

from box import Box
box_one = Box(inside_dict={'data': 5})
box_two = Box(inside_dict={'folly': True})
box_one.merge_update(box_two)
repr(box_one)
"<Box: {'inside_dict': {'data': 5, 'folly': True}}>"

Camel Killer Box now changes keys on insertion

There was a bug in the 4.0 code that meant camel killer was not working at all under normal conditions due to the change of how the box is instantiated.

from box import Box

my_box = Box({'CamelCase': 'Item'}, camel_killer_box=True)
assert my_box.camel_case == 'Item'
print(my_box.to_dict())
# {'camel_case': 'Item'}

Conversion keys are now a bit smarter with how they are handled

Keys with safety underscores used to be treated internally as if the underscores didn’t always exist, i.e.

from box import Box
b = Box(_out = 'preserved')
b.update({'out': 'updated'})
# expected:
# {'_out': 'preserved', 'out': 'updated'}
# observed:
# {'_out': 'updated'}

Those issues have been (hopefully) overcome and now will have the expected <Box: {‘_out’: ‘preserved’, ‘out’: ‘updated’}>

YAML 1.2 default instead of 1.1

ruamel.yaml is now an install requirement and new default instead of PyYAML. By design ruamel.yaml uses the newer YAML v1.2 (which PyYAML does not yet support as of Jan 2020).

To use the older version of 1.1, make sure to specify the version while using the from_yaml methods.

from box import Box
Box.from_yaml("fire_ze_missiles: no")
<Box: {'fire_ze_missiles': 'no'}>

Box.from_yaml("fire_ze_missiles: no", version='1.1')
<Box: {'fire_ze_missiles': False}>

You can read more about the differences here

To use PyYAML instead of ruamel.yaml you must install box without dependencies (such as –no-deps with pip)

If you do chose to stick with PyYaML, you can suppress the warning on just box’s import:

import warnings
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    from box import Box

Additional changes

  • Default Box will also work on None placeholders

Removed

No more Python 2 support

Python 2 is soon officially EOL and Box 4 won’t support it in anyway. Box 3 will not be updated, other than will consider PRs for bugs or security issues.

Removing Ordered Box

As dictionaries are ordered by default in Python 3.6+ there is no point to continue writing and testing code outside of that.

Removing BoxObject

As BoxObject was not cross platform compatible and had some issues it has been removed.

Removing box_it_up

Everything is converted on creation again, as the speed was seldom worth the extra headaches associated with such a design.