Comparison of dict implementationsΒΆ

FeaturesΒΆ

Package

License

Deterministic iteration order

Frozen/Immutable

dict

βœ… PSF

βœ…

❌

constantdict

βœ… MIT

βœ…

βœ…

immutabledict

βœ… MIT

βœ…

βœ…

immutables.Map

βœ… Apache 2.0

❌

βœ…

frozendict

❌ LGPL-3.0

βœ…

βœ…

pyrsistent.PMap

βœ… MIT

❌

βœ…

PerformanceΒΆ

Non-mutating operationsΒΆ

CodeΒΆ

These results were generated with the following code, found in examples/speed.py in the source distribution:

# Simple speed test for various immutable dict implementations

from timeit import timeit
from typing import Mapping

from frozendict import frozendict
from immutabledict import immutabledict
from immutables import Map
from pyrsistent import pmap

from constantdict import constantdict

for N in (1, 1000):
    print(f"\n============= {N} items")
    for dict_impl in (dict, constantdict, immutabledict, Map, frozendict, pmap):

        # {{{ setup

        basedict = {str(i): i for i in range(N)}
        len_dict = len(basedict)

        # }}}

        name = dict_impl.__name__

        print(name)

        # {{{ Creation

        print("  init\t\t", timeit(f"{name}({basedict})",
                                    number=10000, globals=globals()))

        if name not in ("Map", "pmap"):
            print("  fromkeys\t", timeit(f"{name}.fromkeys(range(len_dict))",
                                        number=10000, globals=globals()))
        else:
            print("  fromkeys\t n/a")

        # }}}

        # {{{ Immutable operations

        if name != "dict":
            print("  hash\t\t", timeit(f"hash({name}({basedict}))",
                                    number=10000, globals=globals()))
        else:
            print("  hash\t\t n/a")

        if name != "dict":
            print("  hash2\t\t", timeit("for i in range(1000): hash(x)",
                                        setup=f"x={name}({basedict})",
                                        number=10000, globals=globals()))
        else:
            print("  hash2\t\t n/a")

        print("  elem_access\t", timeit(f"{name}({basedict})['0']",
                                        number=10000, globals=globals()))

        print("  list(keys)\t", timeit(f"list({name}({basedict}).keys())",
                                    number=10000, globals=globals()))

        # }}}

        # {{{ Mutation

        d: Mapping[str, int] = dict_impl(basedict)

        if name == "dict":
            mut_code = "d2 = d.copy(); d2[1] = 1"
        else:
            mut_code = "d2 = d.set(1, 1)"

        try:
            print("  copy+mutate\t", timeit(f"{mut_code}", number=10000,
                                            globals=globals()))
        except AttributeError:
            print("  copy+mutate\t n/a")

        # }}}

ResultsΒΆ

Results (total time of 10,000 executions) for Python 3.11 on a Mac M1:

dict non-mutation performance small dict non-mutation performance large

Mutating operationsΒΆ

CodeΒΆ

These results were generated with the following code, found in examples/speed_mutation.py in the source distribution:

# Based on https://gist.github.com/1st1/292e3f0bbe43bd65ff3256f80aa2637d

import time
from typing import Any, Dict

import frozendict
import immutabledict
import immutables
import pyrsistent

import constantdict

ITER = 1_000_000
KEY = "5"


for N in (5, 10, 20, 30, 100, 200, 300, 400, 500, 1000):
    print("=============")
    print(f"  # of items: {N}; iterations: {ITER}")
    print()

    cd: constantdict.constantdict[str, Any] = constantdict.constantdict()
    h: immutables.Map[str, Any] = immutables.Map()
    d: Dict[str, Any] = {}
    pm: Any = pyrsistent.pmap()
    idd: immutabledict.immutabledict[str, Any] = immutabledict.immutabledict()
    fd: Any = frozendict.frozendict()

    for i in range(N):
        cd = cd.set(str(i), i)
        h = h.set(str(i), i)
        pm = pm.set(str(i), i)
        d[str(i)] = i
        if hasattr(idd, "set"):
            idd = idd.set(str(i), i)
        fd = fd.set(str(i), i)

    assert len(h) == N
    assert len(d) == N
    assert len(pm) == N
    assert len(cd) == N
    if hasattr(idd, "set"):
        assert len(idd) == N
    assert len(fd) == N

    for i in range(N):
        assert h.get(str(i), "not found") == i

    st = time.monotonic()
    for _ in range(ITER):
        d.get(KEY)
        d.get(KEY)
        d.get(KEY)
        d.get(KEY)
        d.get(KEY)

        d.get(KEY)
        d.get(KEY)
        d.get(KEY)
        d.get(KEY)
        d.get(KEY)

        d2 = d.copy()
        d2.update({"aaa": "aaa"})
        # d2['aaa'] = 'aaa'
        # del d2['1']

    end = time.monotonic() - st
    print(f"  dict copy:\t\t\t{end:.4f}s")

    st = time.monotonic()
    for _ in range(ITER):
        h.get(KEY)
        h.get(KEY)
        h.get(KEY)
        h.get(KEY)
        h.get(KEY)

        h.get(KEY)
        h.get(KEY)
        h.get(KEY)
        h.get(KEY)
        h.get(KEY)

        # h2 = h.delete('aaa', 'aaa')
        # h2 = h.delete('1')
        h2 = h.update({"aaa": "aaa"})

    end = time.monotonic() - st
    print(f"  immutables.Map:\t\t{end:.4f}s")

    st = time.monotonic()
    for _ in range(ITER):
        pm.get(KEY)
        pm.get(KEY)
        pm.get(KEY)
        pm.get(KEY)
        pm.get(KEY)

        pm.get(KEY)
        pm.get(KEY)
        pm.get(KEY)
        pm.get(KEY)
        pm.get(KEY)

        # pm2 = pm.set('aaa', 'aaa')
        # pm2 = pm.delete('1')
        pm2 = pm.update({"aaa": "aaa"})

    end = time.monotonic() - st
    print(f"  pyrsistent.PMap:\t\t{end:.4f}s")

    if hasattr(idd, "set"):
        st = time.monotonic()
        for _ in range(ITER):
            idd.get(KEY)
            idd.get(KEY)
            idd.get(KEY)
            idd.get(KEY)
            idd.get(KEY)

            idd.get(KEY)
            idd.get(KEY)
            idd.get(KEY)
            idd.get(KEY)
            idd.get(KEY)

            # idd2 = idd.set('aaa', 'aaa')
            # idd2 = idd.delete('1')
            idd2 = idd.update({"aaa": "aaa"})

        end = time.monotonic() - st
        print(f"  immutabledict copy:\t\t{end:.4f}s")
    else:
        print("  immutabledict does not have a 'set' method, Skipping...")

    st = time.monotonic()
    for _ in range(ITER):
        cd.get(KEY)
        cd.get(KEY)
        cd.get(KEY)
        cd.get(KEY)
        cd.get(KEY)

        cd.get(KEY)
        cd.get(KEY)
        cd.get(KEY)
        cd.get(KEY)
        cd.get(KEY)

        # cd2 = cd.set('aaa', 'aaa')
        # cd2 = cd.delete('1')
        cd2 = cd.update({"aaa": "aaa"})

    end = time.monotonic() - st
    print(f"  constantdict copy:\t\t{end:.4f}s")

    st = time.monotonic()
    for _ in range(ITER):
        fd.get(KEY)
        fd.get(KEY)
        fd.get(KEY)
        fd.get(KEY)
        fd.get(KEY)

        fd.get(KEY)
        fd.get(KEY)
        fd.get(KEY)
        fd.get(KEY)
        fd.get(KEY)

        fd2 = fd.set("aaa", "aaa")
        # fd2 = fd.delete('1')
        # fd2 = fd.update({"aaa": "aaa"}) # n/a in frozendict

    end = time.monotonic() - st
    print(f"  frozendict copy:\t\t{end:.4f}s")

ResultsΒΆ

Results for Python 3.12 on a Mac M1:

dict mutation performance