Hashing Objects¶
This page describes the
coola.hashing package, which provides deterministic hashing
of Python objects, including nested data structures.
Prerequisites: You'll need to know a bit of Python. For a refresher, see the Python tutorial.
Overview¶
The coola.hashing package computes a stable hash string for arbitrary objects, including
nested containers. Unlike Python's built-in hash(), which is randomized across processes for
strings by default and is not guaranteed to be stable across Python versions, coola.hashing
always returns the same hash for equivalent data. It offers:
- Deterministic hashes - the same input always produces the same output
- Recursive hashing - nested
list,dict,tuple, and other containers are hashed element by element - Type-specific hashers - each Python type is handled by a hasher tailored to it
- Extensibility - register hashers for custom types
Basic Usage¶
The simplest way to hash an object is hash_object:
>>> from coola.hashing import hash_object
>>> len(hash_object({"a": 1, "b": "abc"}))
64
hash_object uses the default global registry, which is pre-configured with hashers for common
built-in types (str, bytes, int, float, bool, complex, list, tuple, dict,
date, datetime, Path, ...). The same input always returns the same hash:
>>> from coola.hashing import hash_object
>>> hash_object([1, 2, 3]) == hash_object([1, 2, 3])
True
Controlling the Hash Length¶
The length argument controls the length of the returned hex string (an even number between 2
and 128, defaults to 64):
>>> from coola.hashing import hash_object
>>> len(hash_object("hello", length=16))
16
Handling Unhashable Types¶
By default, hash_object raises a KeyError if it encounters an object (including a nested
one) for which no hasher is registered:
>>> from coola.hashing import hash_object
>>> try:
... hash_object(object())
... except KeyError as e:
... print("Error")
Error
Pass ignore_unhashable=True to replace unhashable objects with a deterministic placeholder
hash instead of raising an error:
>>> from coola.hashing import hash_object
>>> len(hash_object(object(), ignore_unhashable=True))
64
Type-Specific Hashers¶
Each hasher implements the BaseHasher interface and knows how to hash one kind of object,
delegating to a HasherRegistry to hash any nested values. The built-in hashers include:
StringHasher/hash_string- hashesstrobjects directlyBytesHasher/hash_bytes- hashesbytesobjects directlyReprHasher- hashes an object'srepr(); used for numeric types (int,float,complex,bool) becauserepr()preserves floating point precisionStrHasher- hashes an object'sstr(); more human-readable but does not guarantee round-trip accuracy for floatsDatetimeHasher- hashesdate/datetimeobjects via their ISO 8601 representationPathHasher/hash_path- hashespathlib.Pathobjects via their resolved POSIX form, so logically equivalent paths hash the sameSequenceHasher- hasheslist,tuple,str, and otherSequencetypes element by elementMappingHasher- hashesdictand otherMappingtypes, sorted by key so that insertion order does not affect the resultPydanticModelHasher/hash_pydantic_model- hashes pydanticBaseModelobjects, with configurable handling ofSecretStr/SecretBytesfieldsHashableHasher- hashes objects that implement theSupportsHashprotocol (i.e. that define their ownhash()method)
Each hasher can be used directly:
>>> from coola.hashing import StrHasher, HasherRegistry
>>> registry = HasherRegistry()
>>> hasher = StrHasher()
>>> len(hasher.hash([1, 2, 3], registry=registry))
64
The Hasher Registry¶
HasherRegistry maps Python types to hasher instances and dispatches to the most specific
registered hasher for an object's type, using the Method Resolution Order (MRO):
>>> from coola.hashing import HasherRegistry, SequenceHasher, StrHasher
>>> from collections.abc import Sequence
>>> registry = HasherRegistry({object: StrHasher(), Sequence: SequenceHasher()})
>>> len(registry.hash([1, 2, 3]))
64
The Default Registry¶
get_default_registry returns the singleton registry used internally by hash_object. Because
it is a singleton, any modification made via register_hashers affects every subsequent call:
>>> from coola.hashing import register_hashers, StrHasher
>>> class Point:
... def __init__(self, x, y):
... self.x, self.y = x, y
...
... def __str__(self):
... return f"Point({self.x}, {self.y})"
>>> register_hashers({Point: StrHasher()})
register_hashers raises a RuntimeError if a type is already registered, unless
exist_ok=True is passed to overwrite the existing registration.
If you need an isolated registry instead of mutating the shared singleton, create a
HasherRegistry instance directly, as shown above.
Custom Objects¶
For a class that can compute its own hash, implement the SupportsHash protocol by defining a
hash(registry=..., length=..., ignore_unhashable=...) method. HashableHasher then dispatches
to it, and the registry-based hashers (hash_object, SequenceHasher, MappingHasher, ...) can
hash instances of that class like any other registered type.
See Also¶
coola.equality: For comparing nested data structures for equalitycoola.utils: For utility functions including import helpers