Foundations

Actuality

This article is still relevant because Python's hash() is easy to misunderstand. It looks simple, but people often treat it like a general-purpose unique identifier or a stable persistence value. That is usually fine until a design starts depending on exact hash equality across large integers, processes, or systems.

hash() is for hash tables and dictionary/set behavior. It is not a durable identity scheme.

Thesis

The main mistake is not the truncation itself. The real mistake is treating hash() as if it were an application-level identifier. In Python, hash() is allowed to normalize values to the platform hash width, which means a custom __hash__() that returns a very large integer may not produce the same value once hash(obj) is called.

That makes hash() unsuitable as a direct substitute for a database primary key or any other exact identity value that must survive comparison without transformation.

Core concepts

Three distinctions matter here.

Hash value versus unique identifier. A hash is used to support lookup and bucketing. It is not a promise of uniqueness.

Hashability versus mutability. In Python, hashable objects are usually immutable or behave as if their equality-relevant state does not change.

Custom __hash__() versus final hash(...) result. Your __hash__() implementation may return an arbitrary Python integer, but Python can normalize that value to its internal hash width.

Limits

This article is about a narrow but real gotcha.

  • It does not mean custom __hash__() implementations are bad.
  • It does not mean large integers themselves are a problem in Python.
  • It does not mean hashes are useless for equality-aware containers.

The point is more specific: exact numeric identity should not be encoded by assuming hash(obj) == some_large_id.

Implementation and examples

What hash() is, and what it is not

The built-in hash() function works on hashable Python objects.

hash("John")
hash(("Mary", "Carl"))

hash(["Mary", "Carl"])  # TypeError: unhashable type: 'list'
hash({"name": "Sam"})   # TypeError: unhashable type: 'dict'

Two clarifications matter here.

First, a hash is not the same thing as a cryptographic digest such as SHA-256 or MD5. Python's hash() is not designed for security or durable cross-system identity.

Second, equal objects must produce equal hashes, but different objects are not guaranteed to produce different hashes. A hash supports dictionary and set behavior; it does not prove uniqueness.

Classes and hashability

User-defined classes are not automatically hashable in every useful sense. With dataclasses in particular, the interaction between eq and mutability matters.

For example:

from dataclasses import dataclass


@dataclass
class Person:
    name: str
    age: int


john = Person(name="John", age=30)

hash(john)  # TypeError: unhashable type: 'Person'

That default is protective: if equality depends on mutable fields, hashing becomes dangerous.

A normal custom hash

Suppose we decide that two Person objects should hash from their visible attributes.

from dataclasses import dataclass


@dataclass
class Person:
    name: str
    age: int

    def __hash__(self) -> int:
        return hash((self.name, self.age))


john = Person(name="John", age=30)
another_john = Person(name="John", age=30)
mary = Person(name="Mary", age=30)

assert hash(john) == hash(another_john)
assert hash(john) != hash(mary)

This is a normal use of hashing: derive a hash from equality-relevant state.

It is still not an identity system, but it fits the intended contract better than using a business identifier directly as a hash value.

The tempting shortcut

The trouble starts when a system begins to treat hashes as exact identifiers.

Imagine a later refactor where id_ becomes the real identifier and __hash__() simply returns that integer.

from dataclasses import dataclass


@dataclass
class Person:
    id_: int
    name: str
    age: int

    def __hash__(self) -> int:
        return self.id_


john = Person(id_=1, name="John", age=30)

assert hash(john) == john.id_

For small values, this may appear to work. That is what makes the pattern dangerous.

Where the gotcha appears

Python does not promise that the final result of hash(obj) will preserve an arbitrarily large integer unchanged. Hash values are normalized to the platform hash width.

With a large enough value, this assumption breaks.

from dataclasses import dataclass


@dataclass
class Person:
    id_: int
    name: str
    age: int

    def __hash__(self) -> int:
        return self.id_


john = Person(
    id_=98888888888888888888888888888,
    name="John",
    age=30,
)

print(john.id_)
print(hash(john))

assert hash(john) == john.id_  # AssertionError

The important point is that __hash__() returned the large integer you asked for, but the value exposed by hash(john) was normalized to Python's supported hash width on that platform.

The failure is not random. The assumption was wrong: hash(obj) is not specified as an exact transport mechanism for arbitrary integer identifiers.

Checking hash width

You can inspect the effective hash width directly.

import sys

print(sys.hash_info.width)

That value explains how Python sizes its hash results on your platform.

What to do instead

If id_ is your real identifier, compare id_ directly.

assert john.id_ == another_john.id_

If object equality should be defined by id_, implement __eq__() accordingly and make __hash__() consistent with it, but do not build application logic around the assumption that hash(obj) is the identifier itself.

A safer pattern is this:

from dataclasses import dataclass


@dataclass(frozen=True)
class Person:
    id_: int
    name: str
    age: int

    def __hash__(self) -> int:
        return hash(self.id_)

This still uses the identifier as the equality-relevant source, but it delegates hashing back to Python's normal rules instead of pretending the raw identifier and the final hash value must be numerically identical.

Summary

Python's hash() is easy to misuse because it feels like a compact identity function. It is not. It is an internal hashing interface whose results are shaped by Python's hash rules, including platform-sized normalization.

The practical lesson is simple: do not compare hash(obj) to a large database identifier and assume exact equality. Compare identifiers directly, and use hashing only as hashing.

Recommendations

  • treat hash() as a container-support mechanism, not as a durable identifier
  • do not assume hash(obj) must equal the raw integer returned by __hash__() for very large values
  • compare real identifiers directly instead of through hashes
  • keep __eq__() and __hash__() consistent with each other
  • use cryptographic hashes separately when you need stable external digests

A good Python hash implementation supports equality and lookup. It should not be asked to double as the application's identity model.