Build a Bloom Filter from Scratch in Python for Efficient Checks
Bloom filters are probabilistic data structures that efficiently determine if an item is "definitely not" or "possibly" in a set, using minimal memory. They are ideal for scenarios requiring fast membership checks on vast datasets where a small rate of false positives is acceptable. This article details how to build one from scratch in Python, covering its core components, hash function design, and how to size it for a target error rate.

As software developers, we often encounter scenarios where we need to check for an item's existence within a vast dataset. Traditional data structures like hash sets excel at this, but their memory footprint can become prohibitive when dealing with millions or billions of items, especially if those items are large strings or complex objects. This is where a Bloom filter shines, offering a clever space-time tradeoff for membership queries.
A Bloom filter is a probabilistic data structure designed to tell you one of two things about an item: "definitely not in the set" or "possibly in the set." The former is always correct, while the latter might occasionally be a "false positive." The magic? It achieves this with a fixed, small memory footprint, independent of the size of the items themselves, and lightning-fast lookup times.
Why Use a Bloom Filter?
Imagine a scenario where you need to quickly determine if a URL has been seen before without storing the full URL for every single entry. A standard Python set or hash table would consume memory proportional to both the number of items and their individual sizes. For a million URLs, each averaging fifty bytes, a regular set could easily consume tens of megabytes.
A Bloom filter, however, offers a solution that's vastly more memory-efficient. For the same million items and a one percent error rate, it might only require about 1.2 megabytes – a fixed size regardless of URL length. This makes Bloom filters indispensable in contexts like:
- Databases and Storage Engines: Systems like Cassandra and HBase use them to avoid expensive disk reads by quickly checking if a key might be present in a specific data file.
- Web Browsers and Safe Browsing: Google Chrome once used Bloom filters to locally check URLs against a list of known malicious sites, avoiding network calls for safe links.
- Caches and CDNs: They can track frequently requested items, ensuring an item is only cached after a second request, thus filtering out one-off requests.
The core pattern is always the same: a Bloom filter acts as a fast, memory-efficient gate in front of a more expensive operation, reliably telling you when that operation can be skipped.
The Core Components: Bit Array and Hashes
At its heart, a Bloom filter relies on two simple pieces:
- A Bit Array: A long sequence of bits, all initialized to
0. - Multiple Hash Functions: Several independent hash functions that convert an item into multiple distinct positions within that bit array.
To add an item, you feed it to each hash function. Each hash function produces an index, and you set the bit at each of these calculated indices to 1. To check for an item's presence, you run it through the same hash functions, generating the same set of indices. If all the bits at these positions are 1, the item is considered "possibly present." If even one bit is 0, the item is "definitely absent."
Let's start building our Bloom filter in Python:
python import hashlib
class BloomFilter: def init(self, size, num_hashes): self.size = size # number of bits in the array (m) self.num_hashes = num_hashes # number of hash functions (k) self.bits = [0] * size # every bit starts at 0
Generating Hash Positions with Double Hashing
We need num_hashes distinct and well-distributed positions for each item. A common and elegant method for this is double hashing. We compute two initial, independent hashes, then combine them iteratively to generate the required number of unique positions.
python def _positions(self, item): data = item.encode("utf-8") h1 = int.from_bytes(hashlib.sha256(data).digest()[:8], "big") h2 = int.from_bytes(hashlib.md5(data).digest()[:8], "big") for i in range(self.num_hashes): yield (h1 + i * h2) % self.size
In this _positions method:
- We convert the input
itemto bytes for hashing. sha256andmd5provide two strong, independent hash values (h1andh2). We take the first 8 bytes and convert them to an integer to ensure they're large and random-looking.- The formula
(h1 + i * h2) % self.sizegeneratesnum_hashesdistinct indices. The% self.sizeoperation ensures these indices fit within our bit array.
Adding and Checking for Membership
With our _positions generator, the add and __contains__ methods become straightforward:
python def add(self, item): for idx in self._positions(item): self.bits[idx] = 1
def contains(self, item): return all(self.bits[idx] for idx in self._positions(item))
By implementing __contains__, our BloomFilter class can be used with Python's natural in operator, making checks intuitive:
python bf = BloomFilter(size=1000, num_hashes=4) bf.add("alice") bf.add("bob") print("alice" in bf) # True print("bob" in bf) # True print("carol" in bf) # almost always False
Understanding and Managing False Positives
The "almost always False" for "carol" highlights the Bloom filter's probabilistic nature. Because bits are shared among multiple items, it's possible for all the hash positions corresponding to an un-added item (like "carol") to have been set to 1 by other items. When this happens, the filter reports a "yes" for an item that isn't present – this is a false positive.
Crucially, a Bloom filter never produces a false negative. If an item was added, all its corresponding bits were set to 1 and remain 1, so it will always be reported as "possibly present." This asymmetry is a key feature:
- Definitely not in the set: This is always accurate (no false negatives).
- Possibly in the set: This might be incorrect (false positives are possible).
The rate of false positives is controllable and depends on three factors: the bit array size (m), the expected num_items (n), and the num_hashes (k). You can observe false positives directly by overfilling a small filter:
python bf = BloomFilter(size=200, num_hashes=4) for i in range(100): bf.add(f"user-{i}")
false_hits = sum(f"ghost-{i}" in bf for i in range(1000)) print(false_hits) # Expect a non-zero number, demonstrating the false positive rate.
Sizing for Optimal Performance and Error Rate
To achieve a desired false positive rate (p) for a given number of items (n), you can calculate the optimal size (m) and num_hashes (k) using these formulas:
python import math
def optimal_params(n, p): m = math.ceil(-n * math.log(p) / (math.log(2) ** 2)) # bits needed k = max(1, round((m / n) * math.log(2))) # hashes to use return m, k
print(optimal_params(1_000_000, 0.01)) # Example: About (9,585,059 bits, 7 hashes) for 1M items, 1% error
This calculation shows that for 1 million items and a 1% false positive rate, you'd need roughly 9.6 million bits (about 1.2 MB) and 7 hash functions. This demonstrates the significant memory savings compared to storing the actual items.
A Note on Deletion
One significant limitation of a standard Bloom filter is the inability to reliably delete items. Clearing an item's bits might inadvertently clear bits that other items rely on, leading to false negatives (where an item is present but reported as absent), which violates the core guarantee. If deletion is a requirement, a counting Bloom filter (where each slot holds a counter instead of a single bit) is a common alternative, though it consumes more memory.
Practical Takeaways
| Operation | Cost |
|---|---|
add | O(k) |
in (check) | O(k) |
| space | ~m bits for n items, independent of item size |
Bloom filters are invaluable when:
- Memory is a critical constraint.
- Fast membership checks are needed.
- A small, controllable rate of false positives is acceptable (or can be mitigated by a secondary, more accurate check).
- Deletion of items is not a primary requirement.
By understanding their mechanics and limitations, you can leverage Bloom filters to build highly efficient systems that confidently filter out non-members and defer expensive checks only for possible members.
FAQ
Q: Can a Bloom filter ever produce a false negative?
A: No, a standard Bloom filter cannot produce a false negative. Once an item is added, all its corresponding bits are set to 1 and remain so. Therefore, if an item truly exists in the set, a check will always return "possibly in the set."
Q: How does the number of hash functions (k) impact the Bloom filter's performance and accuracy?
A: The number of hash functions (k) is crucial. Too few hashes, and many items will map to the same few bits, leading to a high false positive rate. Too many hashes, and the add and check operations become slower (O(k)), and the bit array fills up faster, also increasing false positives. The optimal k is calculated to minimize the false positive rate for a given m and n.
Q: Is there a way to clear a Bloom filter to reuse it? A: Yes, you can clear a Bloom filter by simply re-initializing its bit array to all zeros. However, you cannot selectively remove individual items from a populated filter without risking false negatives, as explained above. Re-initializing clears all stored "fingerprints" at once.
Related articles
JPMorgan Chase Taps Seattle for Critical AI Control Layer Development
Global financial giant JPMorgan Chase is making a significant strategic investment in Seattle, establishing a new AI software infrastructure team. This pivotal group will build an "AI control layer" to manage the bank's AI operations, aiming to control costs, protect intellectual property, and prevent vendor lock-in.
Is Your Smart Fridge a Scraper? New Data Uncovers Hidden Botnets
New data from Anubis' honeypot reveals a pervasive scraping problem, with nearly 90% of observed scraper IPs not on traditional threat lists. This global phenomenon is likely driven by compromised smart appliances, highlighting a hidden botnet threat. The findings underscore the need for advanced WAFs and user vigilance in securing IoT devices.
Build Your First Multi-Agent AI System with Python and LangGraph
Building Multi-Agent AI Systems: Plain Python vs. LangGraph As developers, we often tackle complex tasks by breaking them down into smaller, manageable pieces. This principle applies equally to AI systems, especially
Master Excel PivotTables: Summarize Data with Ease
Learn to create, customize, and analyze data with Excel PivotTables in simple, step-by-step instructions. Discover how to prepare your data, use the PivotTable Fields pane, and apply interactive filters like slicers for instant insights. Gain control over large datasets and generate clear reports effortlessly.
Unpacking the 'No Spanish Reading Crisis': Lessons for Developers
The Perceived Crisis of Attention in the Digital Age As software developers, we operate in an ecosystem defined by constant information flow and rapid technological shifts. We're acutely aware of the challenges posed by
startups: The web is now mostly bots. Cloudflare is rebuilding its
Cloudflare has launched Precursor, a new defense system, in response to bots now generating over 57% of all web traffic. Precursor monitors entire user sessions to distinguish humans from sophisticated bots, moving beyond traditional single-check methods. This initiative is part of a broader strategy to classify and manage AI agents, control content reuse, and rebuild the web's foundational infrastructure for a machine-dominated internet.





