<?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[ Hashing - 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[ Hashing - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 28 Jul 2026 03:51:29 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/hashing/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>
        
            <item>
                <title>
                    <![CDATA[ What Your Auth Library Isn't Telling You About Passwords: Hashing and Salting Explained ]]>
                </title>
                <description>
                    <![CDATA[ Before I started building auth into my own projects, I didn't think too deeply about what was happening to passwords behind the scenes. Like most developers, I installed a library, called a hash funct ]]>
                </description>
                <link>https://www.freecodecamp.org/news/passwords-hashing-and-salting-explained/</link>
                <guid isPermaLink="false">69b310eb93256dfc5303de72</guid>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ passwords ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Hashing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Salting ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cryptography ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tilda Udufo ]]>
                </dc:creator>
                <pubDate>Thu, 12 Mar 2026 19:15:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/61e84941-bb32-4029-9d58-39022488d29e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Before I started building auth into my own projects, I didn't think too deeply about what was happening to passwords behind the scenes.</p>
<p>Like most developers, I installed a library, called a hash function, stored the result, and moved on. I see a random string like <code>\(2a11yMMbLgN9uY6J3LhorfU9iu....</code> in my database and assume my user's passwords are unbreakable. I knew it was a hashed password. But what was the <code>\)2a</code>? What was <code>11</code>? And if I couldn't reverse it, how was my app verifying logins at all?</p>
<p>If you've ever used bcrypt, Devise, Django's auth system, or really any authentication library, you've been protected from these details. That's good engineering. But understanding what's actually happening makes you a better developer, and it explains a lot of things that seem confusing or arbitrary until suddenly they don't.</p>
<p>By the end of this article, you'll be able to look at that string and know exactly what every part means.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article is written for developers who have used an auth library before but never looked closely at what it's doing. You don't need a cryptography background. If you've ever hashed a password and moved on, this is for you.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-hashing-vs-encryption">Hashing vs Encryption</a></p>
</li>
<li><p><a href="#heading-why-a-plain-hash-isnt-enough">Why a Plain Hash Isn't Enough</a></p>
</li>
<li><p><a href="#heading-enter-salting">Enter Salting</a></p>
</li>
<li><p><a href="#heading-why-bcrypt-is-slow-and-why-thats-the-point">Why bcrypt Is Slow (and Why That's the Point)</a></p>
</li>
<li><p><a href="#heading-whats-actually-in-your-database">What's Actually in Your Database</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-hashing-vs-encryption">Hashing vs Encryption</h2>
<p>Most developers use the terms <strong>hashing</strong> and <strong>encryption</strong> interchangeably. They're not the same thing, and the difference matters more than you might think.</p>
<p>Encryption is a two-way process. You take data, encrypt it with a key, and you can decrypt it later using that same key (or a related one). This is useful when you need to retrieve the original value. Storing a credit card number you'll need to charge later, or sending a message that the recipient needs to read.</p>
<p>Hashing is different. It's a one-way process. You put data in, you get a fixed-length string out, and there's no key that lets you reverse it. The original value is gone.</p>
<p>That might sound like a limitation. For passwords, it's actually exactly what you want.</p>
<p>Think about it: when a user logs in, you don't need to know their password. You just need to verify that what they typed matches what they set when they signed up. You can do that entirely with hashes. Hash what they typed, compare it to the stored hash, done. You never need the original.</p>
<p>This is why "forgot password" flows always ask you to set a new password rather than sending you your old one. Yes, sending you your old password over email might be risky but the actual reason is that they genuinely can't retrieve it. If they can email you your original password, that's a red flag. It means they stored it in a way that's reversible, which means it's not properly protected.</p>
<h2 id="heading-why-a-plain-hash-isnt-enough">Why a Plain Hash Isn't Enough</h2>
<p>So if hashing is one-way and irreversible, isn't that enough? Just hash every password before storing it and you're done?</p>
<p>Not quite.</p>
<p>The first problem is <strong>rainbow tables</strong>. A <a href="https://en.wikipedia.org/wiki/Rainbow_table">rainbow table</a> is a precomputed database of hashes for common passwords. An attacker who gets hold of your database doesn't need to reverse the hashes. They just look them up. If your user's password is "password123", its <a href="https://en.wikipedia.org/wiki/SHA-2">SHA-256</a> hash is always the same string, and that string is almost certainly already in a rainbow table somewhere.</p>
<p>The second problem is related. If two users have the same password, they'll have the same hash. So if an attacker cracks one, they've cracked all of them. In a database with thousands of users, that's a significant security risk.</p>
<p>Here's what that looks like in practice:</p>
<pre><code class="language-python">import hashlib

# Two users, same password
password = "password123"

hash_one = hashlib.sha256(password.encode()).hexdigest()
hash_two = hashlib.sha256(password.encode()).hexdigest()

print(hash_one == hash_two)  # True, every single time
</code></pre>
<p>The hash is deterministic. The same input always produces the same output. That's useful for a lot of things, but for passwords it creates a real vulnerability.</p>
<p>A plain hash gets you partway there. But it's not enough on its own.</p>
<h2 id="heading-enter-salting">Enter Salting</h2>
<p>The fix for both problems is something called a <strong>salt</strong>. And, no it's not your regular table salt.</p>
<p>A salt is a random string generated uniquely for each password. Before hashing, you combine the salt with the password, then hash the result.</p>
<pre><code class="language-python">import hashlib
import os

password = "password123"

# Generate a random salt
salt = os.urandom(16).hex()

# Combine salt and password, then hash
salted_password = salt + password
hashed = hashlib.sha256(salted_password.encode()).hexdigest()

print(f"Salt: {salt}")
print(f"Hash: {hashed}")
</code></pre>
<p>Now two users with the same password produce completely different hashes, because their salts are different. And because the salt is random and unique, it can't be precomputed into a rainbow table.</p>
<p>Here's the surprising part: <strong>the salt doesn't need to be secret</strong>. It gets stored alongside the hash in your database, in plain text. That might feel wrong at first. If an attacker has your database, they have the salt too.</p>
<p>But that's fine. The salt's job isn't to be secret. Its job is to make each hash unique so that precomputed tables are useless. An attacker who wants to crack a salted hash has to brute force each password individually, from scratch, using that specific salt. They can't reuse work across users.</p>
<p>That's a meaningful increase in the cost of an attack, even when the salt is visible.</p>
<h2 id="heading-why-bcrypt-is-slow-and-why-thats-the-point">Why bcrypt Is Slow (and Why That's the Point)</h2>
<p>Salting solves the rainbow table problem. But there's still a gap. If an attacker has your database and decides to brute force a password, they can just keep guessing. Hash a candidate password with the stored salt, compare it to the stored hash, repeat. With a fast hashing algorithm like SHA-256, a modern GPU can do billions of these comparisons per second.</p>
<p>That's the problem with using a general-purpose hash function for passwords. Algorithms like SHA-256 and MD5 were designed to be fast. That's great for things like verifying file integrity or generating checksums. For passwords, it's a liability.</p>
<p>This is where bcrypt comes in. <a href="https://en.wikipedia.org/wiki/Bcrypt">bcrypt</a> is a password hashing algorithm designed specifically to be slow. Not broken or inefficient by accident, but deliberately, configured-to-be slow. It has a <strong>cost factor</strong> (sometimes called a work factor) that controls how computationally expensive the hashing operation is.</p>
<pre><code class="language-python">import bcrypt

password = b"password123"

# The cost factor is set here (12 is a common production value)
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))

print(hashed)
</code></pre>
<p>Every time you increase the cost factor by 1, the hashing operation takes roughly twice as long. At a cost factor of 12, a single hash might take around 300 milliseconds on your server. That's imperceptible to a user logging in. But for an attacker trying to brute force millions of passwords, it turns a feasible attack into an impractical one.</p>
<p>The other advantage of a configurable cost factor is that you can increase it over time as hardware gets faster. What was slow enough in 2015 might not be slow enough today. bcrypt lets you adapt without changing the algorithm itself.</p>
<h2 id="heading-whats-actually-in-your-database">What's Actually in Your Database</h2>
<p>So far, we've talked about salting and cost factors as separate concepts. Here's the satisfying part: in bcrypt, they're all stored together in a single string. That string sitting in your database contains everything needed to verify a password, and once you know how to read it, it's not mysterious at all.</p>
<p>Here's a typical bcrypt hash:</p>
<pre><code class="language-plaintext">\(2a\)12$yMMbLgN9uY6J3LhorfU9iuLAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO
</code></pre>
<p>Let's break it down:</p>
<ul>
<li><p><code>$2a</code> — the <strong>algorithm version</strong>. This tells your auth library which version of bcrypt was used to generate the hash.</p>
</li>
<li><p><code>$12</code> — the <strong>cost factor</strong>. This is the number we talked about in the previous section. A cost factor of 12 means the hashing operation was run 2¹² times.</p>
</li>
<li><p><code>\(yMMbLgN9uY6J3LhorfU9iu</code> — the <strong>salt</strong>. The first 22 characters after the final <code>\)</code> are the salt, stored right there in plain text alongside the hash. Your auth library reads this back out when verifying a login.</p>
</li>
<li><p><code>LAUwKxyy8w42ubeL4MWy7Fh8B.CH/yO</code> — the <strong>hash</strong> itself. The remaining characters are the actual output of the hashing operation.</p>
</li>
</ul>
<p>When a user logs in, your auth library doesn't need any extra information. It reads the algorithm version, cost factor, and salt directly from the stored string, hashes the login attempt using those same parameters, and compares the result. If they match, the password is correct.</p>
<p>This is why bcrypt verification works even though the salt is never stored separately. It was never separate to begin with.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Next time you see a bcrypt string in your database, you'll know exactly what you're looking at. The algorithm version, the cost factor, the salt, and the hash, all encoded in a single string that your auth library knows how to read.</p>
<p>But the bigger takeaway is this: the libraries we rely on every day aren't magic. They're carefully designed systems built on top of concepts that are worth understanding.</p>
<p>Knowing why bcrypt is slow, why salting works even when the salt is visible, and why fast hash functions like SHA-256 are the wrong tool for passwords makes you a more intentional developer. You'll make better decisions about cost factors, you'll recognise a poorly implemented auth system when you see one, and you'll understand why a data breach where passwords were hashed with MD5 is so much worse than one where bcrypt was used.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Perform Secure Hashing Using Python's hashlib Module ]]>
                </title>
                <description>
                    <![CDATA[ Hashing is a fundamental technique in programming that converts data into a fixed-size string of characters. Unlike encryption, hashing is a one-way process: you can't reverse it to get the original data back. This makes hashing perfect for storing p... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-perform-secure-hashing-using-pythons-hashlib-module/</link>
                <guid isPermaLink="false">69409201ee1f8b8545e218b8</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Hashing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Bala Priya C ]]>
                </dc:creator>
                <pubDate>Mon, 15 Dec 2025 22:56:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765839274048/2110b9d7-00c4-4e85-a69b-7223f21f2ac3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hashing is a fundamental technique in programming that converts data into a fixed-size string of characters. Unlike encryption, hashing is a one-way process: you can't reverse it to get the original data back.</p>
<p>This makes hashing perfect for storing passwords, verifying file integrity, and creating unique identifiers. In this tutorial, you'll learn how to use <a target="_blank" href="https://docs.python.org/3/library/hashlib.html">Python's built-in <code>hashlib</code> module</a> to implement secure hashing in your applications.</p>
<p>By the end of this tutorial, you'll understand:</p>
<ul>
<li><p>How to create basic hashes with different algorithms</p>
</li>
<li><p>Why simple hashing isn't enough for passwords</p>
</li>
<li><p>How to add salt to prevent rainbow table attacks</p>
</li>
<li><p>How to use key derivation functions for password storage</p>
</li>
</ul>
<p><a target="_blank" href="https://github.com/balapriyac/python-basics/tree/main/secure-hashing">You can find the code on GitHub</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this tutorial, you should have:</p>
<ul>
<li><p><strong>Basic Python</strong>: Variables, data types, functions, and control structures</p>
</li>
<li><p><strong>Understanding of strings and bytes</strong>: How to encode strings and work with byte data</p>
</li>
</ul>
<p>No external libraries are required, as <a target="_blank" href="https://docs.python.org/3/library/hashlib.html">hashlib</a> and <a target="_blank" href="https://docs.python.org/3/library/os.html">os</a> are both part of Python's standard library.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-basic-hashing-with-pythons-hashlib">Basic Hashing with Python's hashlib</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-simple-hashing-isnt-enough-for-passwords">Why Simple Hashing Isn't Enough for Passwords</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-adding-salt-to-your-hashes">Adding Salt to Your Hashes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-verifying-salted-passwords">Verifying Salted Passwords</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-using-key-derivation-functions">Using Key Derivation Functions</a></p>
</li>
</ol>
<h2 id="heading-basic-hashing-with-pythons-hashlib">Basic Hashing with Python’s hashlib</h2>
<p>Let's start with the fundamentals. The hashlib module provides access to several hashing algorithms like <a target="_blank" href="https://www.md5hashgenerator.com/">MD5</a>, <a target="_blank" href="https://en.wikipedia.org/wiki/SHA-1">SHA-1</a>, <a target="_blank" href="https://emn178.github.io/online-tools/sha256.html">SHA-256</a>, and more.</p>
<p>Here's how to create a simple SHA-256 hash:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib

<span class="hljs-comment"># Create a simple hash</span>
message = <span class="hljs-string">"Hello, World!"</span>
hash_object = hashlib.sha256(message.encode())
hex_digest = hash_object.hexdigest()

print(<span class="hljs-string">f"Original: <span class="hljs-subst">{message}</span>"</span>)
print(<span class="hljs-string">f"SHA-256 Hash: <span class="hljs-subst">{hex_digest}</span>"</span>)
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">Original: Hello, World!
SHA-256 Hash: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
</code></pre>
<p>Here, we import the hashlib module, encode our string to bytes using <code>.encode()</code> as hashlib requires bytes, not strings.</p>
<p>Then we create a hash object using <code>hashlib.sha256()</code> and get the hexadecimal representation with <code>.hexdigest()</code>.</p>
<p>The resulting hash is always 64 characters long regardless of input size. Meaning you have an output string that is <strong>256 bits long</strong>. As each hexadecimal character requires 4 bits, <strong>the output has 256/4 = 64 hexadecimal characters</strong>. Even changing one character produces a completely different hash.</p>
<p>Let's verify that:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib

<span class="hljs-comment"># Small change, big difference</span>
message1 = <span class="hljs-string">"Hello, World!"</span>
message2 = <span class="hljs-string">"Hello, World?"</span>  <span class="hljs-comment"># Only changed ! to ?</span>

hash1 = hashlib.sha256(message1.encode()).hexdigest()
hash2 = hashlib.sha256(message2.encode()).hexdigest()

print(<span class="hljs-string">f"Message 1: <span class="hljs-subst">{message1}</span>"</span>)
print(<span class="hljs-string">f"Hash 1:    <span class="hljs-subst">{hash1}</span>"</span>)
print(<span class="hljs-string">f"\nMessage 2: <span class="hljs-subst">{message2}</span>"</span>)
print(<span class="hljs-string">f"Hash 2:    <span class="hljs-subst">{hash2}</span>"</span>)
print(<span class="hljs-string">f"\nAre they the same? <span class="hljs-subst">{hash1 == hash2}</span>"</span>)
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">Message 1: Hello, World!
Hash 1:    dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f

Message 2: Hello, World?
Hash 2:    f16c3bb0532537acd5b2e418f2b1235b29181e35cffee7cc29d84de4a1d62e4d

Are they the same? False
</code></pre>
<p>This property is called the <a target="_blank" href="https://en.wikipedia.org/wiki/Avalanche_effect">avalanche effect</a> where a tiny change creates a completely different output.</p>
<h2 id="heading-why-simple-hashing-isnt-enough-for-passwords">Why Simple Hashing Isn't Enough for Passwords</h2>
<p>You might think you can just hash passwords and store them in your database. But there's a problem: attackers use <a target="_blank" href="https://en.wikipedia.org/wiki/Rainbow_table">rainbow tables</a>, which are precomputed databases of hashes for common passwords.</p>
<p>Here's what happens:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib

<span class="hljs-comment"># Simple password hashing (DON'T USE THIS!)</span>
password = <span class="hljs-string">"password123"</span>
hashed = hashlib.sha256(password.encode()).hexdigest()

print(<span class="hljs-string">f"Password: <span class="hljs-subst">{password}</span>"</span>)
print(<span class="hljs-string">f"Hash: <span class="hljs-subst">{hashed}</span>"</span>)
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">Password: password123
Hash: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f
</code></pre>
<p>If two users have the same password, they'll have identical hashes. An attacker who cracks one hash knows the password for all users with that hash.</p>
<p>So how do we handle this? Let’s learn in the next section.</p>
<h2 id="heading-adding-salt-to-your-hashes">Adding Salt to Your Hashes</h2>
<p>The solution is <strong>salting</strong>: adding random data to each password before hashing. This way, even identical passwords produce different hashes.</p>
<p>Here's how to implement salted hashing:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib
<span class="hljs-keyword">import</span> os

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hash_password_with_salt</span>(<span class="hljs-params">password</span>):</span>
    <span class="hljs-comment"># Generate a random salt (16 bytes = 128 bits)</span>
    salt = os.urandom(<span class="hljs-number">16</span>)

    <span class="hljs-comment"># Combine password and salt, then hash</span>
    hash_object = hashlib.sha256(salt + password.encode())
    password_hash = hash_object.hexdigest()

    <span class="hljs-comment"># Return both salt and hash (you need the salt to verify later)</span>
    <span class="hljs-keyword">return</span> salt.hex(), password_hash

<span class="hljs-comment"># Hash the same password twice</span>
password = <span class="hljs-string">"password123"</span>

salt1, hash1 = hash_password_with_salt(password)
salt2, hash2 = hash_password_with_salt(password)

print(<span class="hljs-string">f"Password: <span class="hljs-subst">{password}</span>\n"</span>)
print(<span class="hljs-string">f"First attempt:"</span>)
print(<span class="hljs-string">f"  Salt: <span class="hljs-subst">{salt1}</span>"</span>)
print(<span class="hljs-string">f"  Hash: <span class="hljs-subst">{hash1}</span>\n"</span>)
print(<span class="hljs-string">f"Second attempt:"</span>)
print(<span class="hljs-string">f"  Salt: <span class="hljs-subst">{salt2}</span>"</span>)
print(<span class="hljs-string">f"  Hash: <span class="hljs-subst">{hash2}</span>\n"</span>)
print(<span class="hljs-string">f"Same password, different hashes? <span class="hljs-subst">{hash1 != hash2}</span>"</span>)
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">Password: password123

First attempt:
  Salt: fc24b2d2245ff65b80c5bced38744171
  Hash: 5ce634c05941d25871e7ee334b5c24c75f64c4f6d557db66909fcaa793d869f9

Second attempt:
  Salt: bc8a1f79b07e56b51285557211f88bb0
  Hash: 043599d90b2aa0556265869cead35724c7d9d9d37129d897c6b68bade9e737e6

Same password, different hashes? True
</code></pre>
<p>How this works:</p>
<ul>
<li><p><code>os.urandom(16)</code> generates 16 random bytes, which is our salt</p>
</li>
<li><p>We concatenate the salt and password bytes before hashing</p>
</li>
<li><p>We return both the salt (as <code>hex</code>) and the hash</p>
</li>
<li><p>You must store both the salt and hash in your database</p>
</li>
</ul>
<p>When a user logs in, you retrieve their salt, hash the entered password with that salt, and compare the result to the stored hash.</p>
<h2 id="heading-verifying-salted-passwords">Verifying Salted Passwords</h2>
<p>Now let's create a function to verify passwords against salted hashes:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib
<span class="hljs-keyword">import</span> os

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hash_password</span>(<span class="hljs-params">password, salt=None</span>):</span>
    <span class="hljs-string">"""Hash a password with a salt. Generate new salt if not provided."""</span>
    <span class="hljs-keyword">if</span> salt <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        salt = os.urandom(<span class="hljs-number">16</span>)
    <span class="hljs-keyword">else</span>:
        <span class="hljs-comment"># Convert hex string back to bytes if needed</span>
        <span class="hljs-keyword">if</span> isinstance(salt, str):
            salt = bytes.fromhex(salt)

    password_hash = hashlib.sha256(salt + password.encode()).hexdigest()
    <span class="hljs-keyword">return</span> salt.hex(), password_hash

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">verify_password</span>(<span class="hljs-params">password, stored_salt, stored_hash</span>):</span>
    <span class="hljs-string">"""Verify a password against a stored salt and hash."""</span>
    <span class="hljs-comment"># Hash the provided password with the stored salt</span>
    _, new_hash = hash_password(password, stored_salt)

    <span class="hljs-comment"># Compare the hashes</span>
    <span class="hljs-keyword">return</span> new_hash == stored_hash
</code></pre>
<p>Here’s how you can use the above:</p>
<pre><code class="lang-python">print(<span class="hljs-string">"=== User Registration ==="</span>)
user_password = <span class="hljs-string">"mySecurePassword!"</span>
salt, password_hash = hash_password(user_password)
print(<span class="hljs-string">f"Password: <span class="hljs-subst">{user_password}</span>"</span>)
print(<span class="hljs-string">f"Salt: <span class="hljs-subst">{salt}</span>"</span>)
print(<span class="hljs-string">f"Hash: <span class="hljs-subst">{password_hash}</span>"</span>)

<span class="hljs-comment"># Simulate user login attempts</span>
print(<span class="hljs-string">"\n=== Login Attempts ==="</span>)
correct_attempt = <span class="hljs-string">"mySecurePassword!"</span>
wrong_attempt = <span class="hljs-string">"wrongPassword"</span>

print(<span class="hljs-string">f"Attempt 1: '<span class="hljs-subst">{correct_attempt}</span>'"</span>)
print(<span class="hljs-string">f"  Valid? <span class="hljs-subst">{verify_password(correct_attempt, salt, password_hash)}</span>"</span>)

print(<span class="hljs-string">f"\nAttempt 2: '<span class="hljs-subst">{wrong_attempt}</span>'"</span>)
print(<span class="hljs-string">f"  Valid? <span class="hljs-subst">{verify_password(wrong_attempt, salt, password_hash)}</span>"</span>)
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">=== User Registration ===
Password: mySecurePassword!
Salt: 381779b5262deea84183e4b9454b98b1
Hash: 9756e1f0bc4c1aa4a72f35b0be8d3c8f430d31613371cf7de3c615bc475de98f

=== Login Attempts ===
Attempt 1: 'mySecurePassword!'
  Valid? True

Attempt 2: 'wrongPassword'
  Valid? False
</code></pre>
<p>This implementation shows a complete registration and login flow.</p>
<h2 id="heading-using-key-derivation-functions">Using Key Derivation Functions</h2>
<p>While salted SHA-256 is better than plain hashing, modern applications should use key derivation functions (KDFs) specifically designed for password hashing. These include <a target="_blank" href="https://www.npmjs.com/package/pbkdf2">PBKDF2</a> (Password-Based Key Derivation Function 2), <a target="_blank" href="https://bcrypt-generator.com/">bcrypt</a>, <a target="_blank" href="https://en.wikipedia.org/wiki/Scrypt">scrypt</a>, and <a target="_blank" href="https://en.wikipedia.org/wiki/Argon2">Argon2</a>. You can check the links to learn more about these key derivation functions.</p>
<p>These algorithms are intentionally slow and require more computational resources, making brute-force attacks much harder. Let's implement PBKDF2, which is built into Python:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> hashlib
<span class="hljs-keyword">import</span> os

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hash_password_pbkdf2</span>(<span class="hljs-params">password, salt=None, iterations=<span class="hljs-number">600000</span></span>):</span>
    <span class="hljs-string">"""Hash password using PBKDF2 with SHA-256."""</span>
    <span class="hljs-keyword">if</span> salt <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        salt = os.urandom(<span class="hljs-number">32</span>)  <span class="hljs-comment"># 32 bytes = 256 bits</span>
    <span class="hljs-keyword">elif</span> isinstance(salt, str):
        salt = bytes.fromhex(salt)

    <span class="hljs-comment"># PBKDF2 with 600,000 iterations (OWASP recommendation for 2024)</span>
    password_hash = hashlib.pbkdf2_hmac(
        <span class="hljs-string">'sha256'</span>,          <span class="hljs-comment"># Hash algorithm</span>
        password.encode(), <span class="hljs-comment"># Password as bytes</span>
        salt,              <span class="hljs-comment"># Salt as bytes</span>
        iterations,        <span class="hljs-comment"># Number of iterations</span>
        dklen=<span class="hljs-number">32</span>           <span class="hljs-comment"># Desired key length (32 bytes = 256 bits)</span>
    )

    <span class="hljs-keyword">return</span> salt.hex(), password_hash.hex(), iterations

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">verify_password_pbkdf2</span>(<span class="hljs-params">password, stored_salt, stored_hash, iterations</span>):</span>
    <span class="hljs-string">"""Verify password against PBKDF2 hash."""</span>
    _, new_hash, _ = hash_password_pbkdf2(password, stored_salt, iterations)
    <span class="hljs-keyword">return</span> new_hash == stored_hash

<span class="hljs-comment"># Hash a password</span>
print(<span class="hljs-string">"=== PBKDF2 Password Hashing ==="</span>)
password = <span class="hljs-string">"SuperSecure123!"</span>
salt, hash_value, iterations = hash_password_pbkdf2(password)

print(<span class="hljs-string">f"Password: <span class="hljs-subst">{password}</span>"</span>)
print(<span class="hljs-string">f"Salt: <span class="hljs-subst">{salt}</span>"</span>)
print(<span class="hljs-string">f"Hash: <span class="hljs-subst">{hash_value}</span>"</span>)
print(<span class="hljs-string">f"Iterations: <span class="hljs-subst">{iterations:,}</span>"</span>)
</code></pre>
<p>This outputs:</p>
<pre><code class="lang-plaintext">=== PBKDF2 Password Hashing ===
Password: SuperSecure123!
Salt: b388aecd774f6a7ddd95405091548bb50102c99beb1a10326a4c54070da4a3a5
Hash: c681450f41d0cec9ea2aad1108efe2a430b9c3d9fc3af621071be10ac9b3615a
Iterations: 600,000
</code></pre>
<p>Now let’s verify the password and also compare the speeds of SHA-256 vs. PBKDF2:</p>
<pre><code class="lang-python">print(<span class="hljs-string">"\n=== Verification ==="</span>)
is_valid = verify_password_pbkdf2(password, salt, hash_value, iterations)
print(<span class="hljs-string">f"Password valid? <span class="hljs-subst">{is_valid}</span>"</span>)

<span class="hljs-comment"># Show time comparison</span>
<span class="hljs-keyword">import</span> time

print(<span class="hljs-string">"\n=== Speed Comparison ==="</span>)
test_password = <span class="hljs-string">"test123"</span>

<span class="hljs-comment"># Simple SHA-256</span>
start = time.time()
<span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(<span class="hljs-number">100</span>):
    hashlib.sha256(test_password.encode()).hexdigest()
sha256_time = time.time() - start

<span class="hljs-comment"># PBKDF2</span>
start = time.time()
<span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(<span class="hljs-number">100</span>):
    hash_password_pbkdf2(test_password)
pbkdf2_time = time.time() - start

print(<span class="hljs-string">f"100 SHA-256 hashes: <span class="hljs-subst">{sha256_time:<span class="hljs-number">.3</span>f}</span> seconds"</span>)
print(<span class="hljs-string">f"100 PBKDF2 hashes: <span class="hljs-subst">{pbkdf2_time:<span class="hljs-number">.3</span>f}</span> seconds"</span>)
print(<span class="hljs-string">f"PBKDF2 is <span class="hljs-subst">{pbkdf2_time/sha256_time:<span class="hljs-number">.1</span>f}</span>x slower"</span>)
</code></pre>
<p>Output:</p>
<pre><code class="lang-plaintext">
=== Verification ===
Password valid? True

=== Speed Comparison ===
100 SHA-256 hashes: 0.000 seconds
100 PBKDF2 hashes: 53.631 seconds
PBKDF2 is 240068.1x slower
</code></pre>
<p>How PBKDF2 works:</p>
<ul>
<li><p>Takes your password and salt</p>
</li>
<li><p>Applies the hash function (SHA-256) repeatedly – 600,000 times in this example</p>
</li>
<li><p>Each iteration makes the computation slower and harder to brute-force</p>
</li>
<li><p>You store the salt, hash, AND iteration count (so you can verify later)</p>
</li>
</ul>
<p>The iteration count can be increased over time as computers get faster. Modern recommendations (2024) suggest 600,000 iterations for PBKDF2-SHA256.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've learned how to implement secure password hashing in Python using the <code>hashlib</code> module. Here are the key takeaways:</p>
<ul>
<li><p>Basic hashing with SHA-256 is useful for data integrity, not passwords</p>
</li>
<li><p>Salting prevents rainbow table attacks by making each hash unique</p>
</li>
<li><p>PBKDF2 adds computational cost through iterations, slowing down attackers</p>
</li>
<li><p>Always store the salt, hash, and iteration count together</p>
</li>
<li><p>Use key derivation functions (PBKDF2, bcrypt, Argon2) for passwords</p>
</li>
</ul>
<p>The code examples in this tutorial provide a solid foundation for implementing authentication in your projects. But remember, security is an ongoing process. Stay updated on best practices and regularly review your security implementations.</p>
<p>Happy (secure) coding!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
