BytePane

SHA-256 Hash of a String in JavaScript (Browser & Node) and Python

Hashing6 min read

Quick answer

Browser: await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)) then convert the ArrayBuffer to hex. Node: createHash('sha256').update(str, 'utf8').digest('hex'). Python: hashlib.sha256(str.encode('utf-8')).hexdigest().

All three return the same 64-character hex string for the same UTF-8 bytes. "hello world" hashes to b94d27b9…2efcde9 everywhere.

1. Browser: Web Crypto API

SubtleCrypto.digest() takes the algorithm name and a byte buffer and resolves to an ArrayBuffer. Strings must be encoded to bytes first; TextEncoder always produces UTF-8. MDN lists SHA-1, SHA-256, SHA-384 and SHA-512 as supported, with SHA-1 flagged as vulnerable and not for cryptographic use.

async function sha256Hex(text) {
  const bytes = new TextEncoder().encode(text)            // UTF-8
  const digest = await crypto.subtle.digest('SHA-256', bytes)
  return Array.from(new Uint8Array(digest))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

sha256Hex('hello world').then(console.log)
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

This exact function also runs unchanged in Node 20, because Node exposes the same API as globalThis.crypto. MDN notes that SubtleCrypto is limited to secure contexts (HTTPS, localhost) in some or all browsers, so do not expect it on a plain http:// page. Newer engines add Uint8Array.prototype.toHex(); the map/padStart fallback above works everywhere.

2. Node.js: crypto.createHash

const { createHash } = require('node:crypto')

function sha256Hex(text) {
  return createHash('sha256').update(text, 'utf8').digest('hex')
}

console.log(sha256Hex('hello world'))
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
console.log(sha256Hex(''))
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

// Base64 instead of hex (44 chars, ends with "=")
console.log(createHash('sha256').update('hello world').digest('base64'))
// uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=

update() can be called repeatedly for streams; digest() can be called once. The synchronous API is the right choice on the server; the async Web Crypto version is the right choice in the browser.

3. Python: hashlib

import hashlib

def sha256_hex(text: str) -> str:
    return hashlib.sha256(text.encode('utf-8')).hexdigest()

print(sha256_hex('hello world'))
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

h = hashlib.sha256()
h.update(b'hello ')
h.update(b'world')
print(h.hexdigest())        # same digest, incremental
print(h.digest_size, h.block_size)   # 32 64

hashlib.sha256() accepts bytes only; passing a str raises TypeError. That is a feature: it forces you to choose the encoding explicitly.

Same input, same digest: the cross-check

All three implementations plus the command line, run on the same machine:

InputEncodingSHA-256 (hex)
hello worldUTF-8 (Node, Python, Web Crypto, sha256sum, openssl)b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
héllo wörldUTF-8 (Node and Python agree)a1003f7d04a4115711d0b48a2eaf1359ce565d2d2a6fd65098dfcffadeeef59f
héllo wörldLatin-1 (Python .encode('latin-1'))12d616370ce8314b1af15dec5dd3657c827b146290171fe61689372b1ca21397
(empty string)anye3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
$ printf 'hello world' | sha256sum
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9  -
$ echo -n "hello world" | openssl dgst -sha256
SHA2-256(stdin)= b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

The third row is the classic mismatch: same characters, different bytes, different hash. When two systems disagree, compare the bytes (and check for a trailing \n from echo without -n) before suspecting the hash function.

Files and large inputs: hash in chunks

Reading a whole file into memory just to hash it is unnecessary. Both APIs are incremental, so feed chunks and call the digest once at the end. A file containing exactly hello world (no trailing newline) produces the same digest as the string examples above:

// Node.js: stream the file through the hash
const { createHash } = require('node:crypto')
const { createReadStream } = require('node:fs')

function sha256File(path) {
  return new Promise((resolve, reject) => {
    const hash = createHash('sha256')
    createReadStream(path)
      .on('data', chunk => hash.update(chunk))
      .on('end', () => resolve(hash.digest('hex')))
      .on('error', reject)
  })
}
sha256File('hello.txt').then(console.log)
// b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
# Python: 1 MiB chunks
import hashlib

def sha256_file(path: str) -> str:
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(1 << 20), b''):
            h.update(chunk)
    return h.hexdigest()

print(sha256_file('hello.txt'))
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

sha256sum hello.txt prints the same value, which is the quickest way to confirm your code hashes the bytes you think it does. In the browser, crypto.subtle.digest() has no incremental mode: pass the file's ArrayBuffer (from file.arrayBuffer()) in one call; that works for typical uploads but does not stream.

When SHA-256 is the wrong tool

  • Passwords: use bcrypt, scrypt, or Argon2id (see the password section of our hash functions guide). A fast hash is the attacker's friend.
  • Authenticating a message with a secret: use HMAC-SHA256, not sha256(secret + message). See HMAC-SHA256 in Node, Python and Go.
  • Hiding data: a hash is one-way but not secret; short or predictable inputs can be brute-forced or looked up.

Primary sources: MDN SubtleCrypto.digest(), Node.js crypto.createHash, Python hashlib.

Frequently Asked Questions

Why does my SHA-256 hash differ between JavaScript and Python?

Almost always because the two sides hashed different bytes. SHA-256 hashes bytes, not characters, so both sides must encode the string the same way (UTF-8 is the safe default) and must not include a trailing newline. "héllo wörld" encoded as UTF-8 and as Latin-1 produce completely different digests.

Can I use SHA-256 to store passwords?

No. SHA-256 is designed to be fast, which is exactly what you do not want for password storage. Use a slow, salted password hashing function such as bcrypt, scrypt, or Argon2id. Plain SHA-256 is fine for content checksums, cache keys, deduplication, and as the underlying hash inside HMAC.

Does crypto.subtle.digest work over plain HTTP?

MDN documents SubtleCrypto as available only in secure contexts (HTTPS or localhost) in some or all supporting browsers, so on a plain http:// page crypto.subtle may be undefined. Node.js exposes the same Web Crypto API as globalThis.crypto without that restriction.

How long is a SHA-256 hash?

Always 256 bits, which is 32 bytes. As lowercase hexadecimal that is 64 characters; as standard Base64 it is 44 characters including one padding =. The output length does not depend on the input length; the empty string hashes to e3b0c442...b855.

Try it in the browser

Hash Generator, SHA-256, SHA-512, MD5 and more, computed locally in your browser with the Web Crypto API. Paste hello world and compare with the table above.

Related guides