Box 7 is out! But should you use it?

Box logo

There may be a better solution. Wait, is that the author of Box telling us to use something else? Why yes it is! Not because it’s going away, nor an I’m going way, nor is it dangerous to use or anything like that. Simply because after nearly a decade after Box was released, Python 3.11 has better built-ins and other tools may have better specific use cases for you!

In the beginning…

Box was actually called Namespace in the reusables package back in February of 2014, where it was a simple dot accessible dict and not much else. It wasn’t until 2017 that it was used enough on its own to break it out into its own package and start adding more features.

A lot more features. Over 2000 GitHub stars worth of features!

Do you want a default-dots-shortcut-camel case killer box? You can have it! And some of these features you still can’t easily replicate outside of Box. So it’s not like everyone should switch off to a home brew solution, but plenty of cases are taken care of already. So first, lets see what Box 7 can do.

What Box does

The basics premise Box is designed to solve is simple, making dictionaries more easily accessible with . instead of ["<key>"] so it’s a lot easier to type and read.

from box import Box

my_box = Box({"holy cow, would you look at that": "THAT"}) 

assert my_box.holy_cow_would_you_look_at_that == "THAT"

While it still does that, it also provides a lot of other handy features.

Convert to or from JSON, YAML, msgpack or TOML with ease!

Need to show your dict off via a lot of different messaging formats? Box has you covered!

from box import Box 

my_box = Box(photography={"next best photographer": "@shoot.naked"})                                                  

my_box.to_json()                                                     
# '{"photography": {"next best photographer": "@shoot.naked"}}'

my_box.to_yaml()
# "photography:\n  next best photographer: '@shoot.naked'\n"

my_box.to_toml() 
# '[photography]\n"next best photographer" = "@shoot.naked"\n'

my_box.to_msgpack() 
# b'\x81\xabphotography\x81\xb6next best photographer\xac@shoot.naked'

Box.from_toml('[photography]\n"next best photographer" = "@shoot.naked"\n') 
Box({'photography': {'next best photographer': '@shoot.naked'}})

You can also send them directly to files, or pull them out of files!

my_box.to_json(filename="photo.json")

Box.from_json(filename="photo.json")
Box({'photography': {'next best photographer': '@shoot.naked'}})

Default Box

Don’t want to error out when you don’t have a key in there? Box will go ahead and set it when you request it! Unless you set default_box_create_on_get=False, then it won’t.

from box import Box

my_box = Box(default_box=True)

my_box.level_1.level_2.level_3 
# Box({})

# my_box == Box({'level_1': {'level_2': {'level_3': {}}}})

Or maybe you want to return something different than just a boring empty box?

from box import Box

my_box = Box(default_box=True, default_box_attr="fastflix") 
my_box.the_best_free_video_encoder_made_by_someone_named_Chris
# 'fastflix'

Box Dots

Oh no, your multi level dict got run over and it’s keys flattened into a sting like my_vars.my_beautiful.vars how will you get your dictionary structure back!

# from box import DDBox

# DDBox is same as Box(default_dict=True, box_dots=True) with the benefits of the Shorthand box as well!
fixed = DDBox({"my_vars.my_beautiful.vars": "here I am!"}) 

# fixed
# DDBox({'my_vars': {'my_beautiful': {'vars': 'here I am!'}}})

Frozen Box

Need your dict as iced solid and unyielding as Texas’ power grid in February? Freeze your box! Which really means to just not allow additions or removals after creation.

lotr = Box(you_have_no_power_here="Gandalf", frozen_box=True) 
lotr.you_have_no_power_here = "Gandalf the White"

# Traceback (most recent call last):
#  File "box\box.py", line 666, in box.box.Box.__setattr__
#    raise BoxError("Box is frozen")
# box.exceptions.BoxError: Box is frozen

And a lot more!

There is an entire wiki that tries to cover everything you get in the Box.

Box even {"allowed": "for"} | {"combining": "dicts"} with Union | before it was official (and also allows + which isn’t even in Python!)

Amazing! what can’t it do?

Type hinting, auto-completion, dict like speed, field validation, and being in the standard library are all a bit out of Box’s reach. So before you go running off to install yet another pip module with a simple command like:

pip install python-box[all] --upgrade

Make sure you check out some possibly better alternatives first!

Dataclasses

Python 3.7 dropped with a dope class wrapper called dataclasses. It gives you dot notation access to attributes, with type hinting as part of the standard dictionary. As soon as this releases, Box had to quake in its … boxers?

They give you typing, they give you defaults, they even support being frozen!

from dataclasses import dataclass, field
 
@dataclass
class Photographer:
 
    name: str
    instagram: str
    subject_type: str = field(default="light painting")

me = Photographer("Chris", "@shoot.naked")
me.subject_type 
# "light_painting"

Pydantic

Probably in the top five of best named python modules, pydantic lives up to its name in the best of ways! Not just type “hinting” it’s type enforcement!

from pydantic import BaseModel, validator


class Photographer(BaseModel):
    name: str
    instagram: str
    times_nominated_to_be_a_nobel_prize_winner: int = 0

    @validator('instagram')
    def instagram_must_start_with_at_sign(cls, v):
        if not v.startswith("@"):
            raise ValueError("That's not an instagram account!")
        return v

me = Photographer("Chris", "shoot.naked")

me = Photographer(name="Chris", instagram="shoot.naked")
# Traceback (most recent call last):
# ...
# pydantic.error_wrappers.ValidationError: 1 validation error for Photographer
# instagram
#  That's not an instagram account! (type=value_error)

me = Photographer(name="Chris", instagram="@shoot.naked", times_nominated_to_be_a_nobel_prize_winner="still zero")
# Traceback (most recent call last):
# pydantic.error_wrappers.ValidationError: 1 validation error for Photographer
# times_nominated_to_be_a_nobel_prize_winner
#  value is not a valid integer (type=type_error.integer)

Attrs

attrs came out way before dataclasses but was swiftly forgotten about by many as soon as they had a way to do in base Python. They are a little defensive about it too, but they still pack a lot of features dataclasses don’t have!

There are a lot of different ways to use attrs. Such as defaults, adding metadata to attributes, conversions, slots, freezing them, and more!

defaultdict

Python already has a way to provide defaulted values in a dictionary!

from collections import defaultdict

def function_that_returns_my_default():
    return "Code Calamity"

dd = defaultdict(function_that_returns_my_default)
dd["website"]
# 'Code Calamity'

If you know of other tools that may be good use case specific replacements for Box, drop them in the comments!

There are some closer direct competitors as well! EasyDict, scapl, addict, munch and Prodict all try to solve the dot accessible dictionary problem too.

When should I use Box?

Box for me has always been a nice to have, not a need to have. I love the fast converters and ease of typing with dot notation. And that is mostly the case when dealing with mutable data. If you don’t know the structure of what your data will look like, or only have a loose idea, you can’t package it up neatly in a fancy class of some kind.

It’s also a pain to create a lot of classes if you’re working with a boat load of key names or nested dicts. Whereas Box lets you quickly get deep into the depths of your data.

In summary, attrs is the type of package trusted to go to Mars by NASA, but I’m going to keep Box in my pocket beside my Swiss-army-knife so I have it whenever I need it.