<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Prasanth Madhurapantula - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Prasanth Madhurapantula - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 29 Jun 2026 16:28:35 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/catalysed/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How a Bloom Filter Works: Build One From Scratch in Python ]]>
                </title>
                <description>
                    <![CDATA[ A Bloom filter gives you something that feels like magic: it can tell you whether an item is in a set of billions, using only a few kilobytes of memory. And it answers in the same tiny amount of time  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-bloom-filters-work-build-one-from-scratch-python/</link>
                <guid isPermaLink="false">6a4286cee47e505c8beef4bb</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ data structures ]]>
                    </category>
                
                    <category>
                        <![CDATA[ algorithms ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Hashing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bloom filter ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Prasanth Madhurapantula ]]>
                </dc:creator>
                <pubDate>Mon, 29 Jun 2026 14:53:02 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c34b8589-9a73-4d91-9051-793cff8b2037.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A Bloom filter gives you something that feels like magic: it can tell you whether an item is in a set of billions, using only a few kilobytes of memory. And it answers in the same tiny amount of time no matter how much you have stored.</p>
<p>That sounds impossible. A normal set has to remember every item, so its memory grows with the data. But a Bloom filter remembers almost nothing about the items themselves, yet it still answers membership questions. The catch is that it's allowed to be wrong in one specific, controllable direction.</p>
<p>It's not magic, and the moment you build one yourself, the trick becomes clear and you should understand exactly what it can and can't promise.</p>
<p>In this tutorial, we'll build a working Bloom filter from scratch in Python, using nothing but a list of bits and a couple of hash functions. By the end, you'll understand bit arrays, why we use several hashes, what a false positive is, the one guarantee a Bloom filter never breaks, and how to size one for a target error rate.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-a-bloom-filter-actually-is">What a Bloom Filter Actually Is</a></p>
</li>
<li><p><a href="#heading-a-short-history">A Short History</a></p>
</li>
<li><p><a href="#heading-where-bloom-filters-are-used">Where Bloom Filters Are Used</a></p>
</li>
<li><p><a href="#heading-the-core-idea-a-bit-array-and-a-few-hashes">The Core Idea: a Bit Array and a Few Hashes</a></p>
</li>
<li><p><a href="#heading-turning-an-item-into-positions">Turning an Item into Positions</a></p>
</li>
<li><p><a href="#heading-adding-and-checking">Adding and Checking</a></p>
</li>
<li><p><a href="#heading-false-positives-are-normal">False Positives Are Normal</a></p>
</li>
<li><p><a href="#heading-sizing-it-for-a-target-error-rate">Sizing it for a Target Error Rate</a></p>
</li>
<li><p><a href="#heading-what-it-cannot-do-delete">What it Cannot Do: Delete</a></p>
</li>
<li><p><a href="#heading-putting-it-together">Putting it Together</a></p>
</li>
</ul>
<h2 id="heading-what-a-bloom-filter-actually-is">What a Bloom Filter Actually Is</h2>
<p>A Bloom filter is a <strong>probabilistic data structure</strong>. Its whole job is to answer one question, "is this item in the set?", and it gives one of only two answers:</p>
<ul>
<li><p><strong>Definitely not in the set.</strong> This answer is always correct.</p>
</li>
<li><p><strong>Possibly in the set.</strong> This answer is usually correct, but it's occasionally wrong.</p>
</li>
</ul>
<p>The surprising part is that it answers without storing the items at all. A normal set, like Python's <code>set</code> or a hash table, keeps every item it has seen, so its memory grows with both the number of items and the size of each one.</p>
<p>A Bloom filter keeps only a fixed row of bits. Its size is decided up front and never changes, whether you store short words or long URLs or whole files.</p>
<p>So a Bloom filter isn't really a container. It's closer to a <strong>fingerprint of a set</strong>. You can't ask it to list what's inside, or to hand an item back. You can only ask "have you probably seen this?", and you can trust its "no" completely.</p>
<p>A quick way to picture it: instead of keeping a guest list of names, you keep a wall of light switches. When a guest arrives, you flip a few switches chosen from their name. To check whether someone came, you look at their switches. If any one of them is off, they definitely never arrived. If all of them are on, they probably did, though someone else's name might have flipped those same switches.</p>
<p>That picture also explains why you would reach for one instead of a plain set. For a million URLs averaging fifty bytes each, a real set costs tens of megabytes and grows with the length of the URLs. A Bloom filter for the same million items at a one percent error rate costs about 1.2 megabytes, fixed, no matter how long the URLs are.</p>
<p>When the set is huge, has to live in memory on every machine, or holds large items, that saving is the difference between practical and impossible. The price is the rare false positive, and the usual pattern makes that cheap: a "no" skips an expensive lookup, and a "yes" just triggers the slower exact check you would have run anyway.</p>
<p>The rule of thumb: if you need exact answers, deletion, or the ability to list what is stored, use a real set. If you need a tiny, fast gate that sits in front of an expensive operation and reliably tells you when you can skip it, use a Bloom filter.</p>
<h2 id="heading-a-short-history">A Short History</h2>
<p>The structure is named after Burton Howard Bloom, who described it in a 1970 paper, <a href="https://dl.acm.org/doi/10.1145/362686.362692">"Space/Time Trade-offs in Hash Coding with Allowable Errors"</a>, in Communications of the ACM.</p>
<p>His motivating example was wonderfully ordinary. A program that hyphenated and spell-checked text needed to look words up in a dictionary, and storing the whole dictionary in the tiny memories of 1970 was too expensive. Bloom's idea was to accept a small, controlled rate of mistakes in exchange for a large saving in space. That single trade, allow a little error and save a lot of memory, is why the structure still turns up in so many large systems more than fifty years later.</p>
<h2 id="heading-where-bloom-filters-are-used">Where Bloom Filters Are Used</h2>
<p>You've very likely used software backed by a Bloom filter today. They're important in:</p>
<ul>
<li><p><strong>Databases and storage engines:</strong> Cassandra, HBase, Bigtable, and many log-structured (LSM-tree) stores keep a Bloom filter for each on-disk file. Before a slow disk read, the engine asks the filter "could this key be in this file?" A "no" lets it skip the file entirely, which avoids a huge number of reads.</p>
</li>
<li><p><strong>Safe browsing:</strong> Early versions of Google Chrome checked each URL against a local Bloom filter of known-dangerous sites. A "no" meant safe, with no network call. A "yes" was rare and triggered a real check against the full list.</p>
</li>
<li><p><strong>Caches and CDNs:</strong> A common trick is to cache an item only after it has been requested at least twice. A Bloom filter cheaply remembers "have I seen this once before?", which filters out the flood of one-time requests.</p>
</li>
<li><p><strong>Recommendations:</strong> Medium has described using a Bloom filter to avoid recommending articles you've already read.</p>
</li>
<li><p><strong>Networking and crypto:</strong> Routers use them to spot duplicate packets, and early Bitcoin light clients used them to request relevant transactions without revealing exactly which addresses they cared about.</p>
</li>
</ul>
<p>The shape is always the same. A Bloom filter stands in front of something expensive (a disk read, a network request, a database query) and turns most of those expensive checks into a couple of fast array reads. Now let's build one and see exactly how.</p>
<h2 id="heading-the-core-idea-a-bit-array-and-a-few-hashes">The Core Idea: a Bit Array and a Few Hashes</h2>
<p>A Bloom filter is built on two pieces:</p>
<ol>
<li><p>A <strong>bit array</strong>: a long row of bits, all starting at 0.</p>
</li>
<li><p>A handful of <strong>hash functions</strong> that each turn an item into a position in that array.</p>
</li>
</ol>
<p>To <strong>add</strong> an item, you run it through each hash function, get several positions, and set the bit at each of those positions to 1.</p>
<p>To <strong>check</strong> an item, you run it through the same hash functions and look at those same positions. If every one of them is 1, the item is "probably present". If even one is 0, the item is "definitely absent".</p>
<p>That second answer is the important one. If a bit is still 0, you know for certain you never added anything that would have set it. The filter never misses something it has actually seen.</p>
<p>Here's the whole structure in Python:</p>
<pre><code class="language-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
</code></pre>
<h2 id="heading-turning-an-item-into-positions">Turning an Item into Positions</h2>
<p>We need <code>num_hashes</code> different positions for each item, and they need to be spread out. A common, clean trick is <strong>double hashing</strong>: compute two independent hashes once, then combine them to produce as many positions as you need.</p>
<pre><code class="language-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
</code></pre>
<p>Three things are happening:</p>
<ul>
<li><p><code>sha256</code> and <code>h2</code> from <code>md5</code> give us two big numbers that are stable for the same string and look random across different strings.</p>
</li>
<li><p><code>h1 + i * h2</code> mixes them into a different value for each <code>i</code>, so the positions scatter instead of clumping together.</p>
</li>
<li><p><code>% self.size</code> folds each value into a valid index, from <code>0</code> to <code>size - 1</code>.</p>
</li>
</ul>
<p>Run this for one item and you get <code>num_hashes</code> positions. Those positions are the item's fingerprint inside the filter.</p>
<h2 id="heading-adding-and-checking">Adding and Checking</h2>
<p>Adding sets the bit at every position. Checking asks whether they're all set.</p>
<pre><code class="language-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))
</code></pre>
<p>Defining <code>__contains__</code> lets us use Python's natural <code>in</code> syntax. Let's try it:</p>
<pre><code class="language-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
</code></pre>
<p><code>"carol"</code> was never added, so at least one of its four bits is almost certainly still 0, and the filter reports absence. That's the common case. But notice the words "almost certainly". That hedge is the whole story of the next section.</p>
<h2 id="heading-false-positives-are-normal">False Positives Are Normal</h2>
<p>Bits are shared. With enough items added, the four bits that happen to encode <code>"carol"</code> might all have been set to 1 by other items, even though <code>"carol"</code> itself was never added. When that happens, the filter says "probably present" for something that's absent. That's a <strong>false positive</strong>.</p>
<p>People new to Bloom filters sometimes think this is a bug. It's not. It's the price you pay for using so little memory, and it's tunable. You can watch it happen by cramming many items into a small filter:</p>
<pre><code class="language-python">bf = BloomFilter(size=200, num_hashes=4)
for i in range(100):
    bf.add(f"user-{i}")

# None of these were added, but some will sneak through as "present":
false_hits = sum(f"ghost-{i}" in bf for i in range(1000))
print(false_hits)  # a non-zero number: the false positive rate in action
</code></pre>
<p>The filter is never wrong in the other direction, though. Every <code>user-i</code> you added still returns <code>True</code>, because adding an item sets all of its bits, and those bits never get cleared. This is the one promise a Bloom filter always keeps:</p>
<ul>
<li><p>A "no" is always correct. No false negatives, ever.</p>
</li>
<li><p>A "yes" might be wrong. False positives are possible.</p>
</li>
</ul>
<p>That asymmetry is exactly what makes Bloom filters useful. A web browser can keep a Bloom filter of known-malicious URLs and check every link instantly. A "no" means the link is safe and needs no further work. A "yes" is rare and just triggers a slower, exact check against the real list. The filter turns most lookups into a couple of array reads.</p>
<h2 id="heading-sizing-it-for-a-target-error-rate">Sizing it for a Target Error Rate</h2>
<p>The false positive rate depends on three numbers: the bit array size <code>m</code>, the number of items you expect to add <code>n</code>, and the number of hash functions <code>k</code>. The approximate false positive rate is:</p>
<pre><code class="language-text">p = (1 - e^(-k*n/m)) ** k
</code></pre>
<p>You don't have to guess these. Given the number of items <code>n</code> and a target false positive rate <code>p</code> you can pick the best <code>m</code> and <code>k</code> directly:</p>
<pre><code class="language-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))  # about (9_585_059, 7)
</code></pre>
<p>Read that result carefully. To track one million items with a one percent error rate, you need roughly 9.6 million bits, which is about 1.2 megabytes, and 7 hash functions.</p>
<p>A real <code>set</code> of one million strings would cost far more, and most of that cost grows with the length of the strings. The Bloom filter doesn't care how long the items are, only how many there are.</p>
<h2 id="heading-what-it-cannot-do-delete">What it Cannot Do: Delete</h2>
<p>There's one more honest limitation. You can't remove an item by clearing its bits, because those bits are shared. Clearing the bits for <code>"alice"</code> might also clear a bit that <code>"bob"</code> depends on, and now <code>"bob"</code> would wrongly report as absent, breaking the no-false-negatives promise.</p>
<p>If you need deletion, the standard fix is a <strong>counting Bloom filter</strong>, where each slot is a small counter instead of a single bit. Add increments the counters, remove decrements them, and a slot counts as "set" while its counter is above zero. It costs more memory, which is the usual trade.</p>
<h2 id="heading-putting-it-together">Putting it Together</h2>
<p>Here's what we built and what it costs:</p>
<table>
<thead>
<tr>
<th>Operation</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td><code>add</code></td>
<td>O(k)</td>
</tr>
<tr>
<td><code>in</code> (check)</td>
<td>O(k)</td>
</tr>
<tr>
<td>space</td>
<td>about <code>m</code> bits for <code>n</code> items, independent of item size</td>
</tr>
</tbody></table>
<p>The takeaways:</p>
<ul>
<li><p>A Bloom filter is a bit array plus a few hash functions. Adding sets <code>k</code> bits, checking asks whether those <code>k</code> bits are all set.</p>
</li>
<li><p>A "no" is always correct. A "yes" can be a false positive, and the rate is something you tune with <code>m</code> and <code>k</code>.</p>
</li>
<li><p>It's tiny and fast because it stores fingerprints, not the items, so it forgets what the items actually were.</p>
</li>
<li><p>It can't delete without a counting variant, because bits are shared.</p>
</li>
</ul>
<p>The next time a system tells you "this is definitely not in the cache, skip the lookup" or "this might be a known item, let me double-check", you'll know exactly what's underneath: a row of bits, a few hashes, and one carefully chosen direction in which it's allowed to be wrong.</p>
<p>If you enjoy learning data structures by building them rather than memorizing them, that's the idea behind a learn-by-doing platform I built called <a href="https://iwtlp.com">IWTLP</a>, where this Bloom filter is one of the build-it-yourself exercises in the data engineering track.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
