How does Python's hmac module produce an HMAC-SHA256 tag over a payload?
answer
- Three arguments, one of them mandatory
- The key is bytes, never a str
- Raw bytes or the hex spelling
- A one-shot call skips the object
- Never compare tags with ==
basics
~10 sBuild an object with hmac.new(key, message, "sha256"), then read .digest() for raw bytes or .hexdigest() for a hex string. hmac.digest(key, message, "sha256") is the one-shot form. The key and message must be bytes.
solid answer
~40 s`hmac.new(key, msg, digestmod)` returns an `hmac.HMAC` object; `digestmod` has been mandatory since Python 3.8, and both `key` and `msg` must be bytes-like — passing a `str` raises `TypeError`, so encode first. Read the tag with `.digest()` for raw bytes or `.hexdigest()` for a lowercase hex `str`; `.update()` lets you feed the message in chunks before finalising. For a message you already hold in memory, `hmac.digest(key, msg, "sha256")` is a one-shot equivalent that takes an optimised C path. The key should be unpredictable bytes of at least the digest size — 32 bytes for SHA-256; keys longer than the hash's block size get hashed down first, so a huge key buys nothing. Verify by recomputing the tag and passing both values to `hmac.compare_digest`, never `==`.
code
python · 10 linesimport hmac
import os
key = os.urandom(32)
message = b"digest-batch-2026-09-05|recipient=42"
tag_hex = hmac.new(key, message, "sha256").hexdigest()
one_shot_hex = hmac.digest(key, message, "sha256").hex()
print(len(tag_hex), tag_hex == one_shot_hex)go deeper
Recall the shape of the call: hmac.new(key, message, "sha256"), then .hexdigest(). Remember that both key and message are bytes, and that you verify by recomputing and calling hmac.compare_digest rather than using ==.
Be ready to explain what .digest(), .hexdigest() and .update() each do, why digestmod is mandatory since 3.8, and when the one-shot hmac.digest is the better call. Know that hex and raw bytes are one value in two spellings.
An interviewer expects you to talk about where the key comes from and what shape it has in transit — 32 unpredictable bytes, decoded from its stored encoding rather than UTF-8 encoded — and to catch the str-versus-bytes boundary before it becomes a tag that never verifies.
Own the decision of where signing lives: one small module that owns key material, algorithm choice and encoding, versus the same three lines copy-pasted into every caller. The second shape is how a service ends up with two incompatible signing schemes and no way to rotate a key.
## What the module gives you The `hmac` module implements a keyed message authentication code: a tag computed from a secret key and a message, such that anyone holding the key can recompute it and nobody else can forge it. In Python that is a three-line operation, and almost all of the interview value is in the details of those three lines. `hmac.new(key, msg=None, digestmod='')` returns an `hmac.HMAC` object. Three things about its arguments trip people up: - **`digestmod` is required.** It was optional and defaulted to MD5 in very old versions; that default was deprecated and then removed, and since **Python 3.8** omitting it raises `TypeError`. Pass the algorithm as a string — `"sha256"` — which lets the module pick the fastest available implementation. Passing the callable `hashlib.sha256` also works but gives up that fast path in some builds. - **`key` and `msg` must be bytes-like.** `bytes`, `bytearray` or `memoryview` are accepted; a `str` raises `TypeError: key: expected bytes or bytearray`. This is the single most common first error, and the fix is an explicit `.encode("utf-8")` at a boundary you control rather than a scattered sprinkling of encodes. - **`msg` is optional.** Omit it and feed the message with `.update()` instead, which is how you sign something you are streaming rather than holding whole in memory. ## Reading the tag out An `hmac.HMAC` object mirrors the `hashlib` interface. `.digest()` returns the raw tag as `bytes` — 32 bytes for SHA-256. `.hexdigest()` returns the same value as a 64-character lowercase hex `str`. `.update(data)` mixes more bytes in and returns `None`; `.copy()` snapshots the running state, which is useful when a fixed prefix is shared by many messages. `.digest_size` and `.block_size` report the underlying hash's sizes. Raw versus hex matters at the wire boundary, not cryptographically: they are the same value in two spellings. Pick whichever the protocol on the other side specifies, and be explicit about it, because a hex string compared against raw bytes will simply never match. ## The one-shot form `hmac.digest(key, msg, digest)` computes the tag in a single call and returns raw bytes. It exists because the object-oriented path allocates and does Python-level work per call; the one-shot form takes an optimised C path when the digest is named by string, and it is measurably faster for the short messages that dominate signing work such as tokens and webhook bodies. There is no `hexdigest` twin — call `.hex()` on the result. ## Choosing the key An HMAC key is not a password. It is unpredictable binary key material, and the right size is *at least* the digest size — 32 bytes for SHA-256. Longer keys are not stronger in any meaningful way: HMAC hashes any key longer than the hash's block size (64 bytes for SHA-256) down to a digest before use, so a 4 KB key and its 32-byte hash are the same key. Shorter keys are genuinely weaker, and a human-chosen phrase used directly as an HMAC key is guessable in a way that random bytes are not. A key living in an environment variable arrives as a `str`, so it must be decoded to the exact bytes both sides agreed on — usually hex or base64, not a UTF-8 encode of the printable form. Getting this wrong produces tags that never verify, and the symptom looks identical to a wrong key. ## Verifying To check a tag, recompute it over the same message with the same key and compare. Use `hmac.compare_digest(a, b)`, which takes two `bytes` values or two ASCII-only `str` values and returns a bool, rather than `==`. Mixing the two forms — a `str` from `.hexdigest()` against `bytes` from the wire — raises `TypeError`, which is a useful accident because it catches the mismatch at the boundary. ## Why not just hash the key with the message The reason `hmac` exists as a module rather than as a documentation note is that concatenating a secret and a message and hashing the pair is not equivalent. HMAC's construction hashes twice, with the key mixed into two different padded blocks, and that structure is what the security proof rests on. Reaching for `hashlib.sha256(key + message)` because it is one call shorter is the mistake the module was written to prevent.
- Why does the hmac module exist at all instead of people calling hashlib.sha256 on the key joined to the message?Because that concatenation is not a sound authentication code. HMAC feeds the key through two padded passes of the hash rather than prepending it, and that nested structure is what its security argument rests on. A bare digest of a secret followed by a message inherits the internal state weaknesses of the hash construction, which is exactly what the standardised scheme was designed to avoid. In Python the correct call is not meaningfully longer, so there is no reason to hand-roll it.
- Does using a very long HMAC key make the tag harder to forge?No. HMAC hashes any key longer than the hash function's block size — 64 bytes for SHA-256 — down to a single digest before use, so a 4 KB key and its 32-byte digest are literally the same key. The useful floor is the digest size, 32 bytes for SHA-256, of unpredictable binary key material. Beyond that you are storing and rotating bytes that never reach the computation.
- When would you choose hmac.digest over building an hmac.HMAC object?Whenever the whole message is already in memory and you need the tag once. `hmac.digest` takes an optimised single-call path and avoids the per-call object construction, which shows up when you are signing or verifying many short messages such as tokens or webhook bodies. Keep `hmac.new` plus `.update()` for messages you stream in chunks, or when you want `.copy()` to reuse a shared prefix across many tags.
saying these in an interview costs you the question
- Passing a str key and being surprised by TypeError
- Omitting digestmod and expecting a sensible default
- Claiming hashlib.sha256(key + message) is the same thing
- Comparing tags with == instead of hmac.compare_digest
- Treating a hex tag and raw digest bytes as interchangeable on the wire
- Using a human-chosen passphrase directly as the HMAC key