HMAC Python β€” hmac.new() SHA-256 Guide + Code Examples

Β·DevOps Engineer & Python Automation SpecialistΒ·Reviewed byMaria SantosΒ·Published

Use the free online HMAC Generator directly in your browser β€” no install required.

Try HMAC Generator Online β†’

Every webhook callback, every signed API request, every Stripe or GitHub event notification uses an HMAC signatureto prove the payload was not tampered with. Python's hmac module handles HMAC-SHA256 in Python with a single function call: hmac.new(key, msg, hashlib.sha256). No pip install, no C extension, no third-party dependency. For quick one-off signature checks without writing code, the online HMAC Generator gives you the result instantly. This guide covers hmac.new(), hmac.digest(), hmac.compare_digest(), Base64 encoding, webhook verification, API request signing, and every hash algorithm from SHA-1 to SHA-512. All examples target Python 3.7+.

  • βœ“hmac.new(key, msg, hashlib.sha256) is the standard entry point β€” key and msg must be bytes, digestmod is mandatory since Python 3.4.
  • βœ“hmac.digest(key, msg, "sha256") is a faster one-shot alternative added in Python 3.7 β€” returns raw bytes, no intermediate object.
  • βœ“Always verify signatures with hmac.compare_digest() to prevent timing attacks β€” never use == for HMAC comparison.
  • βœ“Base64-encode the raw .digest() output for HTTP headers and webhook signatures: base64.b64encode(h.digest()).
  • βœ“The hmac module accepts any hashlib algorithm: sha1, sha256, sha384, sha512, md5, blake2b.

What is HMAC?

HMAC (Hash-based Message Authentication Code) is a construction defined in RFC 2104 that combines a secret key with a hash function to produce a fixed-size authentication tag. Unlike a plain hash (which anyone can compute), an HMAC requires knowledge of the secret key. This means you can use it to verify both the integrity and the authenticity of a message. If even a single byte of the message or the key changes, the output is completely different. The construction works by hashing the key XORed with two different padding constants (ipad and opad), wrapping the message between two hash operations. Python's hmac module implements this RFC directly.

Before Β· Python
After Β· Python
# Plain SHA-256 hash β€” no secret key, anyone can compute
hashlib.sha256(b"payment:9950:USD").hexdigest()
# "7a3b1c..."  (deterministic, public)
# HMAC-SHA256 β€” requires the secret key to produce
hmac.new(b"api_secret", b"payment:9950:USD", hashlib.sha256).hexdigest()
# "e4f2a8..."  (only key holder can compute)

hmac.new() β€” The Standard Library Entry Point

The hmac module is part of the Python standard library. Two imports and you are ready: import hmac, hashlib. The three main functions are hmac.new() (creates an HMAC object), hmac.digest() (one-shot, Python 3.7+), and hmac.compare_digest() (constant-time comparison). No pip install required.

hmac.new(key, msg, digestmod) takes three arguments. Both key and msg must be bytes-like objects ( bytes, bytearray, or memoryview). The digestmod argument is mandatory since Python 3.4 and accepts any hashlib constructor (like hashlib.sha256) or a string name like "sha256".

Python 3.7+ β€” minimal HMAC-SHA256 example
import hmac
import hashlib

key = b"webhook_signing_key_2026"
message = b'{"event":"invoice.paid","invoice_id":"inv_8f3a","amount":19900}'

# Create the HMAC object and get the hex signature
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
print(signature)
# "b4e74f6c9a1d3e5f8b2a7c0d4e6f1a3b5c7d9e0f2a4b6c8d0e1f3a5b7c9d0e2f"

The HMAC object exposes two output methods. .digest() returns raw bytes (32 bytes for SHA-256, 64 for SHA-512). .hexdigest() returns a lowercase hex string. The hex string is a plain Python str β€” no decoding step needed.

Python 3.7+ β€” .digest() vs .hexdigest()
import hmac
import hashlib

key = b"service_auth_key"
msg = b"GET /api/v2/orders 2026-03-28T14:30:00Z"

h = hmac.new(key, msg, hashlib.sha256)

raw_bytes = h.digest()
print(type(raw_bytes), len(raw_bytes))
# <class 'bytes'> 32

hex_string = h.hexdigest()
print(type(hex_string), len(hex_string))
# <class 'str'> 64

# They represent the same data β€” hex is just a string encoding of the bytes
assert raw_bytes.hex() == hex_string

If your key or message is a Python string, call .encode() to convert it to bytes before passing it to hmac.new(). This trips up almost everyone the first time β€” Python 3 strings are Unicode, not bytes, and the hmac module refuses them.

Python 3.7+ β€” encoding strings to bytes
import hmac
import hashlib

# String key and message β€” .encode() converts to UTF-8 bytes
api_key = "sk_live_9f3a2b7c4d8e"
request_body = '{"customer_id":"cust_4421","plan":"enterprise"}'

signature = hmac.new(
    api_key.encode(),
    request_body.encode(),
    hashlib.sha256
).hexdigest()

print(signature)
# "3a9f1b..."  β€” consistent hex string output
Note:The digestmod parameter has no default since Python 3.4. Calling hmac.new(key, msg) without it raises TypeError. Before 3.4, it defaulted to MD5, which is why the Python maintainers removed the default β€” forcing you to make an explicit, safe choice.

HMAC-SHA256 Base64, SHA-1, SHA-512, and MD5

The hmac.new() function works with any hash algorithm available in hashlib. Most webhook providers and API gateways use HMAC-SHA256, but you will encounter SHA-1 in OAuth 1.0a, SHA-512 in protocols that mandate it, and MD5 in legacy systems that have not been updated.

HMAC-SHA256 with Base64 Output

Many webhook providers send the signature as a Base64-encoded string in an HTTP header. To produce the same format, pass the raw .digest() bytes to base64.b64encode().

Python 3.7+ β€” HMAC-SHA256 Base64 encoding
import hmac
import hashlib
import base64

key = b"whsec_MbkP7x9yFqHGn3tRdWz5"
payload = b'{"id":"evt_1Nq","type":"charge.succeeded","data":{"amount":4200}}'

# Raw digest β†’ Base64 (common for Authorization headers and webhook signatures)
raw_digest = hmac.new(key, payload, hashlib.sha256).digest()
b64_signature = base64.b64encode(raw_digest).decode("ascii")

print(b64_signature)
# "dGhpcyBpcyBhIHNhbXBsZSBzaWduYXR1cmU="

# This is the value you'd compare against the X-Signature header
header_value = f"sha256={b64_signature}"
print(header_value)
# "sha256=dGhpcyBpcyBhIHNhbXBsZSBzaWduYXR1cmU="

HMAC-SHA1 β€” Legacy Protocol Compatibility

SHA-1 is considered weak for new designs, but HMAC-SHA1 is still required by OAuth 1.0a and some older webhook implementations. The code is identical β€” just swap the algorithm.

Python 3.7+ β€” HMAC-SHA1
import hmac
import hashlib

consumer_secret = b"oauth_consumer_secret_2026"
token_secret = b"oauth_token_secret_2026"

# OAuth 1.0a uses consumer_secret&token_secret as the signing key
signing_key = consumer_secret + b"&" + token_secret
base_string = b"GET&https%3A%2F%2Fapi.service.com%2Fv1%2Forders&oauth_nonce%3D7f3a91bc"

sig = hmac.new(signing_key, base_string, hashlib.sha1).digest()

import base64
oauth_signature = base64.b64encode(sig).decode("ascii")
print(oauth_signature)
# "Tza3R9sE..."  β€” URL-encode this for the Authorization header

HMAC-SHA512 β€” Longer Output

Python 3.7+ β€” HMAC-SHA512
import hmac
import hashlib

key = b"high_security_signing_key_64_bytes_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
msg = b'{"transfer_id":"xfr_9c2e","amount":500000,"currency":"EUR"}'

h = hmac.new(key, msg, hashlib.sha512)

print(len(h.digest()))   # 64 bytes (512 bits)
print(len(h.hexdigest()))  # 128 hex characters
print(h.hexdigest()[:40] + "...")
# "8e3a1f9b2c4d6e7f0a1b3c5d7e9f0a2b4c6d8e0f..."

HMAC-MD5 β€” Legacy Only

Python 3.7+ β€” HMAC-MD5
import hmac
import hashlib

# MD5 is cryptographically broken β€” use only for legacy protocol compatibility
key = b"legacy_api_key"
msg = b"action=charge&amount=1500&merchant=store_42"

sig = hmac.new(key, msg, hashlib.md5).hexdigest()
print(sig)  # 32-character hex string
# "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
Warning:HMAC-MD5 is acceptable only for backward compatibility with systems you cannot migrate. For any new project, use HMAC-SHA256 at minimum. MD5 has known collision vulnerabilities that, while less directly exploitable in HMAC mode, make it a poor default choice.

hmac.new() Parameters Reference

The constructor signature is hmac.new(key, msg=None, digestmod). All three functions in the module share the same pattern for key and algorithm arguments.

hmac.new() constructor

Parameter
Type
Default
Description
key
bytes | bytearray
(required)
The secret key β€” must be bytes, not a string
msg
bytes | None
None
Initial message to hash; more data can be added via .update()
digestmod
str | callable
(required)
Hash algorithm β€” e.g. hashlib.sha256 or the string "sha256"

hmac.digest() one-shot (Python 3.7+)

Parameter
Type
Description
key
bytes
The secret key
msg
bytes
The message to authenticate
digest
str | callable
Hash algorithm β€” same as digestmod in hmac.new()

The digestmod parameter accepts either a callable (like hashlib.sha256) or a string name (like "sha256"). The callable form is preferred because it is validated at import time β€” a typo in the string form only fails at runtime.

hmac.digest() β€” Fast One-Shot HMAC (Python 3.7+)

Python 3.7 added hmac.digest(key, msg, digest) as a module-level function. It computes the HMAC in a single call without creating an intermediate HMAC object. The return value is raw bytes (equivalent to calling .digest() on the object). This function uses an optimized C implementation on CPython and avoids the object allocation overhead, making it measurably faster in tight loops.

Python 3.7+ β€” hmac.digest() one-shot
import hmac
import hashlib

key = b"batch_signing_key_2026"
messages = [
    b'{"order_id":"ord_001","total":4500}',
    b'{"order_id":"ord_002","total":8900}',
    b'{"order_id":"ord_003","total":2200}',
]

# One-shot digest β€” no intermediate HMAC object
signatures = [hmac.digest(key, msg, hashlib.sha256) for msg in messages]

# Convert to hex for display
for msg, sig in zip(messages, signatures):
    print(f"{msg[:30]}... -> {sig.hex()[:24]}...")

The limitation: hmac.digest() only returns raw bytes. If you need the hex string directly, you still need hmac.new() for its .hexdigest() method, or chain .hex() on the bytes result.

Note:hmac.digest() does not support incremental .update() calls. If you are reading a large file in chunks and need to HMAC the contents, stick with hmac.new() and call .update(chunk) in a loop.

Verify HMAC Signature from Webhook and API Response

The most common use of HMAC in Python is verifying webhook signatures. Every major provider (Stripe, GitHub, Shopify, Twilio) signs payloads with HMAC-SHA256 and sends the signature in a header. The pattern is always the same: recompute the HMAC over the raw request body, then compare with hmac.compare_digest().

Webhook Signature Verification

Python 3.7+ β€” webhook HMAC verification (Flask)
import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = b"whsec_MbkP7x9yFqHGn3tRdWz5"

@app.route("/webhooks/payments", methods=["POST"])
def handle_payment_webhook():
    # Get the raw body β€” must match exactly what was signed
    raw_body = request.get_data()

    # Get the signature from the header
    received_sig = request.headers.get("X-Signature-256", "")

    # Recompute the HMAC over the raw body
    expected_sig = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()

    # Constant-time comparison β€” prevents timing attacks
    if not hmac.compare_digest(f"sha256={expected_sig}", received_sig):
        abort(403, "Invalid signature")

    # Signature verified β€” process the event
    event = request.get_json()
    print(f"Verified event: {event['type']} for {event['data']['amount']}")
    return "", 200

The hmac.compare_digest() function compares two strings or byte sequences in constant time. A regular == comparison short-circuits on the first mismatched byte. An attacker can measure the response time across many requests and gradually reconstruct the expected signature byte by byte. Constant-time comparison eliminates this side channel.

GitHub Webhook Verification

GitHub's webhook format illustrates the full pattern. It sends an X-Hub-Signature-256 header containing sha256= followed by the hex-encoded HMAC-SHA256 of the raw request body, signed with the webhook secret you configure in your repository settings. The key difference from generic webhook verification is that you must strip the sha256= prefix before comparing, and you must read the raw bytes of the request body β€” parsing JSON first changes the byte representation and breaks verification.

Python 3.7+ β€” GitHub X-Hub-Signature-256 verification
import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
GITHUB_WEBHOOK_SECRET = b"your_github_webhook_secret"

@app.route("/webhooks/github", methods=["POST"])
def handle_github_webhook():
    # GitHub sends: X-Hub-Signature-256: sha256=<hex_digest>
    sig_header = request.headers.get("X-Hub-Signature-256", "")

    if not sig_header.startswith("sha256="):
        abort(403, "Missing or malformed signature header")

    received_hex = sig_header[len("sha256="):]
    raw_body = request.get_data()  # raw bytes β€” do not parse JSON before this

    expected_hex = hmac.new(
        GITHUB_WEBHOOK_SECRET, raw_body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected_hex, received_hex):
        abort(403, "Signature mismatch β€” payload may have been tampered with")

    event_type = request.headers.get("X-GitHub-Event", "unknown")
    payload = request.get_json()
    print(f"Verified GitHub {event_type} event: {payload.get('action', '')}")
    return "", 200

The same pattern applies to Shopify (X-Shopify-Hmac-SHA256) and Twilio (X-Twilio-Signature), with the only difference being the header name and whether the signature is hex or Base64-encoded. Always check the provider docs to confirm the encoding format β€” mixing hex and Base64 is the most common cause of signature mismatch errors.

API Request Authentication with HMAC

Some APIs require the client to sign each request with HMAC. The signed string usually includes the HTTP method, path, timestamp, and request body. Here is a pattern I use for internal service-to-service authentication.

Python 3.7+ β€” signing API requests with HMAC-SHA256
import hmac
import hashlib
import time
import json

def sign_request(secret: bytes, method: str, path: str, body: str) -> dict:
    """Generate HMAC-SHA256 signature for an API request."""
    timestamp = str(int(time.time()))

    # Build the signing string β€” method + path + timestamp + body
    signing_string = f"{method}\n{path}\n{timestamp}\n{body}"

    signature = hmac.new(
        secret,
        signing_string.encode(),
        hashlib.sha256
    ).hexdigest()

    return {
        "X-Timestamp": timestamp,
        "X-Signature": signature,
    }

# Usage
api_secret = b"sk_hmac_9f3a2b7c4d8e1a6f"
body = json.dumps({"customer_id": "cust_4421", "action": "suspend_account"})

headers = sign_request(api_secret, "POST", "/api/v2/customers/actions", body)
print(headers)
# {"X-Timestamp": "1711612200", "X-Signature": "a3f1b9c0..."}

Signing HTTP Requests with the requests Library

Python 3.7+ β€” HMAC-signed requests with the requests library
import hmac
import hashlib
import time
import json
import requests

API_SECRET = b"sk_hmac_9f3a2b7c4d8e1a6f"
BASE_URL = "https://api.billing-service.internal"

def make_signed_request(method: str, path: str, payload: dict) -> requests.Response:
    body = json.dumps(payload, separators=(",", ":"))  # compact JSON, deterministic
    timestamp = str(int(time.time()))

    signing_string = f"{method}\n{path}\n{timestamp}\n{body}"
    signature = hmac.new(API_SECRET, signing_string.encode(), hashlib.sha256).hexdigest()

    headers = {
        "Content-Type": "application/json",
        "X-Timestamp": timestamp,
        "X-Signature": f"hmac-sha256={signature}",
    }

    try:
        return requests.request(method, f"{BASE_URL}{path}", data=body, headers=headers)
    except requests.RequestException as e:
        raise RuntimeError(f"Signed request failed: {e}") from e

# Send a signed POST request
resp = make_signed_request("POST", "/api/v2/invoices", {
    "customer_id": "cust_4421",
    "line_items": [
        {"description": "Pro plan - March 2026", "amount": 4900},
        {"description": "Extra seats (3)", "amount": 2100},
    ],
})
print(resp.status_code, resp.json())

Quick note: use separators=(",", ":") when serializing the body for signing. The default json.dumps() adds spaces after separators, which changes the byte representation and breaks signature verification if the server serializes differently. Compact JSON gives you a canonical form.

Command-Line HMAC Generation

Sometimes you need to compute an HMAC without writing a script. Python's -c flag and openssl both handle this from the terminal.

bash β€” HMAC-SHA256 via Python one-liner
python3 -c "
import hmac, hashlib
print(hmac.new(b'my_secret', b'message_to_sign', hashlib.sha256).hexdigest())
"
# outputs: 64-character hex string
bash β€” HMAC-SHA256 via openssl (for comparison)
echo -n "message_to_sign" | openssl dgst -sha256 -hmac "my_secret"
# SHA2-256(stdin)= 7d11...

# Pipe a file through openssl HMAC
openssl dgst -sha256 -hmac "my_secret" < payload.json
bash β€” HMAC from environment variable key
# Store key in env var to avoid shell history exposure
export HMAC_KEY="sk_live_9f3a2b"
echo -n '{"event":"test"}' | python3 -c "
import hmac, hashlib, sys, os
key = os.environ['HMAC_KEY'].encode()
msg = sys.stdin.buffer.read()
print(hmac.new(key, msg, hashlib.sha256).hexdigest())
"
Note:The echo -n flag is critical β€” without it, echo appends a newline character to the message, which changes the HMAC output. This is the most common cause of signature mismatch when debugging from the terminal.

High-Performance Alternative β€” cryptography Library

For most applications, the standard hmac module is fast enough. If you are already using the cryptography library for TLS or certificate handling, it also provides HMAC backed by OpenSSL. The main practical difference from the stdlib is the exception-based .verify() API described below β€” it raises on mismatch instead of returning a boolean you might forget to check.

bash β€” install cryptography
pip install cryptography
Python 3.7+ β€” HMAC via the cryptography library
from cryptography.hazmat.primitives import hashes, hmac as crypto_hmac

key = b"webhook_signing_key_2026"
message = b'{"event":"subscription.renewed","plan":"enterprise"}'

h = crypto_hmac.HMAC(key, hashes.SHA256())
h.update(message)
signature = h.finalize()  # raw bytes

print(signature.hex())
# "9c4e2a..."

# Verify mode β€” raises InvalidSignature on mismatch
h_verify = crypto_hmac.HMAC(key, hashes.SHA256())
h_verify.update(message)
h_verify.verify(signature)  # raises cryptography.exceptions.InvalidSignature if wrong

The cryptography library's .verify() method is particularly nice: it raises an exception on mismatch instead of returning a boolean. This makes it harder to accidentally ignore a verification failure. The standard library's hmac.compare_digest() returns True/ False, which can be silently ignored if the developer forgets to check the return value.

Terminal Output with Syntax Highlighting

If you are debugging HMAC signatures in the terminal and want colored output, rich handles this well.

bash β€” install rich
pip install rich
Python 3.7+ β€” colored HMAC output with rich
import hmac
import hashlib
from rich.console import Console
from rich.table import Table

console = Console()

key = b"debug_signing_key"
messages = {
    "/api/v2/orders": b'{"status":"active"}',
    "/api/v2/invoices": b'{"status":"pending"}',
    "/api/v2/customers": b'{"status":"verified"}',
}

table = Table(title="HMAC-SHA256 Signatures")
table.add_column("Endpoint", style="cyan")
table.add_column("Signature (first 32 chars)", style="green")

for endpoint, body in messages.items():
    sig = hmac.new(key, body, hashlib.sha256).hexdigest()
    table.add_row(endpoint, sig[:32] + "...")

console.print(table)
Note:Rich is for terminal display only. Do not use it when writing HMAC signatures to files, HTTP headers, or log aggregation systems β€” the ANSI escape codes will corrupt the output.

Working with Large Files β€” Incremental HMAC

For files over 50 MB or so, loading everything into memory just to compute an HMAC is wasteful. The .update() method on the HMAC object lets you feed data in chunks. This keeps memory usage constant regardless of file size.

Python 3.7+ β€” HMAC a large file in chunks
import hmac
import hashlib

def hmac_file(key: bytes, filepath: str, chunk_size: int = 8192) -> str:
    """Compute HMAC-SHA256 of a file without loading it entirely into memory."""
    h = hmac.new(key, digestmod=hashlib.sha256)

    try:
        with open(filepath, "rb") as f:
            while True:
                chunk = f.read(chunk_size)
                if not chunk:
                    break
                h.update(chunk)
    except OSError as e:
        raise OSError(f"Cannot read file '{filepath}': {e}") from e

    return h.hexdigest()

# Sign a 2 GB database export
signing_key = b"backup_integrity_key_2026"
signature = hmac_file(signing_key, "/var/backups/db-export-2026-03.sql.gz")
print(f"HMAC-SHA256: {signature}")
Python 3.7+ β€” verify HMAC signature of a downloaded file
import hmac
import hashlib

def verify_file_hmac(key: bytes, filepath: str, expected_hex: str) -> bool:
    """Verify the HMAC-SHA256 signature of a file."""
    h = hmac.new(key, digestmod=hashlib.sha256)

    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)

    return hmac.compare_digest(h.hexdigest(), expected_hex)

# Verify a downloaded artifact
is_valid = verify_file_hmac(
    key=b"release_signing_key",
    filepath="/tmp/release-v3.2.0.tar.gz",
    expected_hex="8e3a1f9b2c4d6e7f0a1b3c5d7e9f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f",
)
print(f"File integrity: {'valid' if is_valid else 'CORRUPTED'}")
Note:Switch to chunked HMAC when the file exceeds 50-100 MB or when processing uploads in a web server where memory per request matters. The .update() approach uses a fixed chunk_size of memory regardless of file size. I default to 64 KB chunks β€” large enough to amortize syscall overhead, small enough to stay inside L2 cache on most hardware.

Generate a Cryptographically Secure HMAC Key in Python

A weak or predictable key undermines the entire HMAC construction. The secrets module (Python 3.6+) provides cryptographically strong random bytes. For HMAC-SHA256, use a 32-byte key. For HMAC-SHA512, use 64 bytes. These match the internal block size of the respective hash algorithms, which is the optimal key length per RFC 2104.

Python 3.7+ β€” generate HMAC keys with secrets
import secrets

# Generate keys matching the hash algorithm's block size
sha256_key = secrets.token_bytes(32)   # 256 bits β€” for HMAC-SHA256
sha512_key = secrets.token_bytes(64)   # 512 bits β€” for HMAC-SHA512

# Hex representation β€” safe for config files and environment variables
print(f"HMAC-SHA256 key: {sha256_key.hex()}")
# e.g. "a3f1b9c04e7d2f8a1b3c5d7e9f0a2b4c6d8e0f1a2b3c4d5e6f7a8b9c0d1e2f"

print(f"HMAC-SHA512 key: {sha512_key.hex()}")
# 128-character hex string

# URL-safe Base64 β€” compact, safe for HTTP headers
import base64
b64_key = base64.urlsafe_b64encode(sha256_key).decode("ascii")
print(f"Base64 key: {b64_key}")
# e.g. "o_G5wE59L4obPF1-nwortG2ODwobPExdXnqLnA0dLi8="
Warning:Never use random.random() or random.randbytes() from the random module for HMAC keys. The default random module uses the Mersenne Twister PRNG, which is predictable after observing 624 outputs. Always use secrets.token_bytes() for security-sensitive randomness.

Key Length and RFC 2104 Requirements

RFC 2104 specifies that the HMAC key can be any length, but recommends a key of at least L bytes β€” where L is the output length of the underlying hash function. For HMAC-SHA256, that is 32 bytes (256 bits). Keys shorter than L bits reduce the security margin proportionally. Keys longer than the hash's block size (64 bytes for SHA-256, 128 bytes for SHA-512) are first hashed down to the block size, so there is no benefit to using keys longer than the block size. Stick to 32 bytes for HMAC-SHA256 and 64 bytes for HMAC-SHA512.

Secure Key Storage and Rotation

Never hardcode HMAC keys in source code. The standard approach for production deployments is to load the key from an environment variable at startup: os.environ["HMAC_SECRET"].encode(). For higher-assurance environments, store keys in a secrets management system such as AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager and fetch them at runtime. These systems provide audit logs, access controls, and automatic rotation without requiring a code deploy.

Plan for key rotation from the beginning. When a key is rotated, there is a window where in-flight requests were signed with the old key and will fail verification against the new one. The standard mitigation is a brief overlap period: accept signatures from both the old and new key for a short time (minutes to hours), then retire the old key. If a key is compromised β€” exposed in logs, leaked via a git commit, or disclosed in an incident β€” rotate immediately and treat all signatures produced with the compromised key as untrusted. Re-verify any cached verification results and notify downstream consumers of the key change.

Using bytearray and memoryview with hmac.new()

The hmac.new() function accepts any bytes-like object for both the key and msg parameters. This means you can pass bytes, bytearray, or memoryview directly, without copying or converting. This matters most in two scenarios: network protocol implementations where socket.recv_into() writes data into a pre-allocated bytearray buffer, and high-throughput systems where avoiding intermediate copies reduces GC pressure. A memoryview slice is zero-copy: it exposes a window into the original buffer without allocating new memory. At tens of thousands of messages per second, eliminating those allocations makes a measurable difference in latency and throughput.

Python 3.7+ β€” bytearray and memoryview with HMAC
import hmac
import hashlib

# bytearray β€” mutable bytes, useful for binary protocol buffers
key = bytearray(b"protocol_signing_key")
frame = bytearray(b'\x01\x02\x03\x04payload_data_here')

sig = hmac.new(key, frame, hashlib.sha256).hexdigest()
print(f"Frame signature: {sig[:32]}...")

# memoryview β€” zero-copy slice of a larger buffer
large_buffer = bytearray(4096)
large_buffer[:20] = b"sensor_reading_12345"

# HMAC only the first 20 bytes without copying
view = memoryview(large_buffer)[:20]
sig = hmac.new(key, view, hashlib.sha256).hexdigest()
print(f"Sensor signature: {sig[:32]}...")

Common Mistakes

I see the first two mistakes in nearly every code review involving webhook handlers. They are easy to introduce under time pressure and hard to spot without knowing what to look for.

❌ Comparing HMAC signatures with == instead of hmac.compare_digest()

Problem: The == operator short-circuits on the first byte mismatch, leaking timing information that lets an attacker reconstruct the expected signature incrementally.

Fix: Always use hmac.compare_digest() for signature comparison β€” it runs in constant time regardless of where the mismatch occurs.

Before Β· Python
After Β· Python
received_sig = request.headers["X-Signature"]
expected_sig = hmac.new(key, body, hashlib.sha256).hexdigest()

if received_sig == expected_sig:  # VULNERABLE to timing attack
    process_webhook(body)
received_sig = request.headers["X-Signature"]
expected_sig = hmac.new(key, body, hashlib.sha256).hexdigest()

if hmac.compare_digest(received_sig, expected_sig):  # constant-time
    process_webhook(body)
❌ Passing a string instead of bytes to hmac.new()

Problem: hmac.new() requires bytes-like objects. Passing a Python str raises TypeError: "key: expected bytes or bytearray, but got 'str'".

Fix: Call .encode() on string keys and messages before passing them to hmac.new().

Before Β· Python
After Β· Python
key = "my_api_secret"  # str, not bytes
msg = '{"event":"test"}'  # str, not bytes
sig = hmac.new(key, msg, hashlib.sha256)  # TypeError!
key = "my_api_secret"
msg = '{"event":"test"}'
sig = hmac.new(key.encode(), msg.encode(), hashlib.sha256)
❌ Forgetting digestmod (Python 3.4+)

Problem: Calling hmac.new(key, msg) without the third argument raises TypeError. Before Python 3.4 it defaulted to MD5, but the default was removed for security reasons.

Fix: Always pass the algorithm explicitly: hashlib.sha256, hashlib.sha512, or whatever your protocol requires.

Before Β· Python
After Β· Python
# Missing digestmod β€” raises TypeError in Python 3.4+
sig = hmac.new(key, msg).hexdigest()
# Always specify the hash algorithm
sig = hmac.new(key, msg, hashlib.sha256).hexdigest()
❌ Using .hexdigest() when the provider expects Base64

Problem: Many webhook providers (Stripe, Shopify) send Base64-encoded signatures, not hex. Comparing a hex string against a Base64 value always fails, causing all webhooks to be rejected.

Fix: Check the provider's documentation for the signature format. If they use Base64, encode the raw .digest() bytes, not the .hexdigest() string.

Before Β· Python
After Β· Python
# Provider sends Base64, but we compute hex β€” never matches
expected = hmac.new(key, body, hashlib.sha256).hexdigest()
# "a3f1b9c0..."  vs  "o/G5wE59..."  β€” always mismatch
import base64
# Match the provider's format: raw bytes β†’ Base64
raw = hmac.new(key, body, hashlib.sha256).digest()
expected = base64.b64encode(raw).decode("ascii")
# "o/G5wE59..."  β€” matches the header

stdlib hmac vs cryptography β€” Quick Comparison

Method
Algorithm
Output
Streaming
Custom Types
Requires Install
hmac.new() + hexdigest()
Any hashlib
Hex string
βœ“ via .update()
N/A
No (stdlib)
hmac.new() + digest()
Any hashlib
Raw bytes
βœ“ via .update()
N/A
No (stdlib)
hmac.digest()
Any hashlib
Raw bytes
βœ— (one-shot)
N/A
No (stdlib, 3.7+)
hashlib.sha256 (plain hash)
SHA-256 only
Hex or bytes
βœ“ via .update()
N/A
No (stdlib)
cryptography (HMAC)
Any
Raw bytes
βœ“ via .update()
N/A
pip install
pyca/cryptography + CMAC
AES-CMAC
Raw bytes
βœ“ via .update()
N/A
pip install

Use the stdlib hmac module for webhook verification, API signing, and general HMAC operations β€” it requires zero dependencies and covers every standard algorithm. Use hmac.digest() for batch operations where the one-shot speed matters. Reach for the cryptography library only when you already depend on it for other operations (TLS, X.509, symmetric encryption) and want the exception-based .verify() API. For quick signature checks without writing any Python, use the HMAC Generator tool to paste your key and message and get the result instantly.

Frequently Asked Questions

What is the difference between hmac.new() and hmac.digest() in Python?

hmac.new() returns an HMAC object that supports incremental .update() calls and gives you both .digest() (raw bytes) and .hexdigest() (hex string). hmac.digest() is a one-shot function added in Python 3.7 that returns raw bytes directly without creating an intermediate object. Use hmac.digest() when you have the complete message and just need the result. Use hmac.new() when you need to feed data in chunks or need the hex output.

Python
import hmac, hashlib

key = b"webhook_secret_2026"
body = b'{"event":"payment.completed","amount":9950}'

# One-shot β€” returns raw bytes
raw = hmac.digest(key, body, hashlib.sha256)

# Object-based β€” supports incremental updates and hex output
h = hmac.new(key, body, hashlib.sha256)
hex_sig = h.hexdigest()

How do I verify an HMAC signature in Python?

Recompute the HMAC over the original message using the shared secret, then compare using hmac.compare_digest(). Never use == for signature comparison. The == operator is vulnerable to timing attacks because it short-circuits on the first mismatched byte, leaking information about the expected signature length and content.

Python
import hmac, hashlib

def verify_signature(secret: bytes, message: bytes, received_sig: str) -> bool:
    expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received_sig)

Is Python hmac SHA-256 the same as hashing with hashlib.sha256?

No. hashlib.sha256 computes a plain SHA-256 hash of the input, which anyone can reproduce. HMAC-SHA256 mixes a secret key into the hash computation following RFC 2104, so only someone with the key can produce or verify the correct output. A plain hash proves data integrity. An HMAC proves both integrity and authenticity.

Python
import hmac, hashlib

msg = b"transfer:9950:USD"
key = b"api_secret_k8x2"

plain_hash = hashlib.sha256(msg).hexdigest()  # anyone can compute this
hmac_hash = hmac.new(key, msg, hashlib.sha256).hexdigest()  # requires the key

Can I use HMAC-SHA1 in Python 3?

Yes, pass hashlib.sha1 as the digestmod argument. HMAC-SHA1 still works fine in Python 3 and the hmac module has no deprecation warnings for it. That said, SHA-1 is considered weak for new designs β€” its collision resistance is below 80 bits and NIST deprecated it for most digital signature uses in 2015. The main reason to use HMAC-SHA1 today is backward compatibility with existing protocols that mandate it, like OAuth 1.0a or certain legacy webhook systems. When you control both sides of the integration, prefer HMAC-SHA256 or HMAC-SHA512 for all new work.

Python
import hmac, hashlib

key = b"oauth_consumer_secret"
base_string = b"GET&https%3A%2F%2Fapi.example.com&oauth_nonce%3Dabc123"
sig = hmac.new(key, base_string, hashlib.sha1).digest()

How do I generate a secure HMAC key in Python?

Use secrets.token_bytes() from the standard library. For HMAC-SHA256, a 32-byte key is the standard recommendation since it matches the hash block size. For HMAC-SHA512, use 64 bytes. Do not use random.random() or os.urandom() for key generation in application code β€” secrets is the correct module for security-sensitive randomness since Python 3.6.

Python
import secrets

hmac_sha256_key = secrets.token_bytes(32)  # 256 bits
hmac_sha512_key = secrets.token_bytes(64)  # 512 bits

# Store as hex for config files
print(hmac_sha256_key.hex())
# e.g. "a3f1b9c04e..."

Why does hmac.new() require digestmod in Python 3?

Before Python 3.4, digestmod defaulted to MD5, which is cryptographically broken β€” MD5 has known collision attacks and should never be used in new security-sensitive code. The Python maintainers removed the default to force an explicit algorithm choice, preventing developers from silently shipping MD5-based MACs. If you call hmac.new(key, msg) without digestmod, you get a TypeError. Always pass the algorithm explicitly: hashlib.sha256, hashlib.sha512, or any other hashlib constructor. When in doubt, hashlib.sha256 is the safe default β€” no known weaknesses and fast enough for any practical workload.

Python
import hmac, hashlib

key = b"secret"
msg = b"data"

# This raises TypeError in Python 3.4+
# hmac.new(key, msg)  # TypeError: missing required argument: 'digestmod'

# Always specify the algorithm
h = hmac.new(key, msg, hashlib.sha256)

For a quick HMAC check without spinning up a Python script, paste your key and message into the online HMAC Generator β€” it supports SHA-256, SHA-384, and SHA-512 with instant results.

DV
Dmitri VolkovDevOps Engineer & Python Automation Specialist

Dmitri is a DevOps engineer who relies on Python as his primary scripting and automation language. He builds internal tooling, CI/CD pipelines, and infrastructure automation scripts that run in production across distributed teams. He writes about the Python standard library, subprocess management, file processing, encoding utilities, and the practical shell-adjacent Python that DevOps engineers use every day.

MS
Maria SantosTechnical Reviewer

Maria is a backend developer specialising in Python and API integration. She has broad experience with data pipelines, serialisation formats, and building reliable server-side services. She is an active member of the Python community and enjoys writing practical, example-driven guides that help developers solve real problems without unnecessary theory.