Track 3 · Lesson 2 of 5

Hashing and encoding in Python

Turn text into bytes, take digests, and round-trip through base64, in whole programs built from blanks.


Python is strict about one thing most languages let you be vague about: the difference between text and bytes. A str is characters. A bytes is raw values. Hashing and encoding both work on bytes, so encode() is the doorway you have to walk through, and every program below walks through it.

What you'll be able to do

  • Explain why len("café") and len("café".encode()) are different numbers.
  • Round-trip bytes through hex and back.
  • Take a SHA-256 digest and read its length.
  • Encode to base64 and decode it again.
  • Write a small function that uses all of it.

Step 1: Characters and bytes are different things

Count the characters in café, then count the bytes.

Characters and bytes Checkpoint
text = "café"
raw =
print("characters", )
print("bytes", len(raw))

Tap a piece to place it

Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.

Four characters, five bytes. The é is one character but it takes two bytes in UTF-8. That gap is the whole reason the distinction exists: the byte length depends on the encoding, the character length does not.

Step 2: Round-trip through hex

Hex is how you look at bytes, because each byte is exactly two hex digits. This program goes to hex and back again.

Bytes to hex and back Checkpoint
message = "Hi"
raw = message.encode()
as_hex =
back = bytes.fromhex()
print(as_hex)
print()

Tap a piece to place it

Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.

Eight lines, and it comes back to where it started, which is what "round trip" means. H is 48 and i is 69. bytes.fromhex takes the hex back to bytes, and the print shows the b because it is still bytes, not text.

Every checksum you have ever seen is this: bytes written out as hex so you can read and compare them.

Step 3: A digest, and how long it is

A hash function takes any amount of data and gives back a fixed number of bytes. SHA-256 always gives 32, whatever went in.

A SHA-256 digest Checkpoint
import hashlib
message = b"hello termux"
digest = hashlib.(message)
as_hex =
print(len(as_hex))
print()

Tap a piece to place it

Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.

Eight lines, two values, one printed figure of 64. That is 32 bytes written as two hex characters each. sha1 would give 40 and md5 would give 32, which is how you recognise a digest on sight: by its length.

as_hex[:8] is the first eight characters, which is the usual way to show a digest on a screen.

Step 4: Base64, both ways

Base64 turns bytes into text using 64 safe characters, so binary can travel through anything that only carries text.

Encode and decode base64 Checkpoint
import base64
raw = b"hello termux"
encoded = base64.(raw)
decoded = base64.(encoded)
print(encoded)
print()

Tap a piece to place it

Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.

Eight lines, and the second print is the original message back again. Both are bytes, which is why both print with a b. Add .decode() if you need a string to put in a JSON field.

Step 5: Why base64url exists

Standard base64 uses + and /. Both mean something in a URL, so a second alphabet swaps them for - and _.

Two alphabets Checkpoint
import base64
raw = bytes([255, 255, 255])
standard =
url_safe =
print(standard)
print(url_safe)

Tap a piece to place it

Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.

The same three bytes of 255 give //// in the standard alphabet and ____ in the URL-safe one. This is exactly what a JWT uses, which is why a token never contains a + or a /.

Step 6: A small tool that uses all of it

Now a function, called in a loop. This is the shape of a real script rather than a demonstration.

A short-hash function Checkpoint
import hashlib
def short_hash(text):
raw =
digest = hashlib.sha256(raw).hexdigest()
return
for word in ["alpha", "bravo"]:
print(word, )

Tap a piece to place it

Turn the switch off when you want to place the pieces yourself, and the mistakes become yours to find.

Ten lines. The function does the work once, and the loop calls it for each word. Two different words give two different digests, and each is eight characters because of the slice.

This is the part that makes a function worth writing: change the slice to digest[:12] and every call changes with it.

Step 7: Type one yourself

Print the first eight characters of the SHA-256 digest of b"password".

A short digest Checkpoint

Runs in your browser, and nothing is sent anywhere. Python starts loading in the background as you reach this cell, about six megabytes once. After that every cell on the site runs instantly until you close the tab.

Step 8: A mistake to find

This one raises. Press Run and read the last line.

Find the mistake Find the mistake

Runs in your browser, and nothing is sent anywhere. Python starts loading in the background as you reach this cell, about six megabytes once. After that every cell on the site runs instantly until you close the tab.

TypeError: Strings must be encoded before hashing. Not cryptic once you know the rule, and the rule is the one thing this lesson is really about.

Quick reference

PieceWhat it does
"text".encode()text to bytes
b"text"a bytes literal
raw.hex()bytes as hex digits
bytes.fromhex(s)hex back to bytes
hashlib.sha256(...)a digest, which needs bytes
.hexdigest()64 hex characters for SHA-256
base64.b64encode(...)bytes to base64 text
base64.b64decode(...)base64 text back to bytes
base64.urlsafe_b64encode(...)the alphabet a URL or JWT can carry
digest[:8]a short digest for a screen
TypeError: Strings must be encodedyou hashed text instead of bytes

Next steps

Test yourself on this

4 questions and 1 task where you run a real command, with the reasoning explained either way.

Start