GROUND
Problem
What becomes confusing, fragile, or impossible without understanding bytes, text, and encodings? This lesson answers that through explanation, a worked example, two runnable exercises, and a reference solution. No teacher-supplied worksheet is required.
Before integrated development environments, the operating system, terminal, files, and text streams were the environment. Their small composable interfaces still underpin modern tooling.
LEARN
Concept explanation
Files contain bytes, not characters. Text appears only after software interprets those bytes with an encoding.
Unicode assigns code points to abstract characters. UTF-8 encodes each code point as one to four bytes, so character count and byte count are different measurements.
A reader using wrong encoding can produce mojibake or reject input even when stored bytes are unchanged. Always record encoding at system boundaries.
Eight bits. Files, sockets, and memory ultimately move byte sequences.
Unicode number identifying an abstract character, such as U+00E9 for é.
Rule mapping code points to bytes and bytes back to code points.
Example bank
Compare normal, boundary, failure, and cross-layer cases. Predict each observation before revealing the explanation.
SETUPEncode A (U+0041) as UTF-8.
OBSERVEod prints 41; wc -c reports 1.
WHY IT MATTERSUTF-8 preserves ASCII byte values, which is why ASCII protocols remain readable inside UTF-8.
SETUPEncode composed é (U+00E9).
OBSERVEod prints c3 a9; wc -c reports 2 although the screen shows one grapheme.
WHY IT MATTERSVisible characters, Unicode code points, and storage bytes are different measurement layers.
SETUPCompare composed é with e (U+0065) plus combining acute (U+0301).
OBSERVEThe forms can look identical; bytes are c3 a9 versus 65 cc 81.
WHY IT MATTERSVisual equality does not imply byte equality. Normalize only where the product contract requires it.
SETUPCompare 🙂 in UTF-8 and JavaScript string length.
OBSERVEUTF-8 uses f0 9f 99 82 (4 bytes); JavaScript .length is 2 UTF-16 code units; [...text].length is 1 code point.
WHY IT MATTERSAlways name the unit being counted: bytes, code units, code points, or grapheme clusters.
SETUPRead lone ff as UTF-8.
OBSERVEff can never start a valid UTF-8 sequence; tools reject it or emit a replacement character.
WHY IT MATTERSDecoder policy—reject, replace, or preserve bytes—is an explicit system-boundary decision.
SEE
Worked example
Encode three characters as UTF-8 and compare their bytes.
- ASCII A is U+0041 → hexadecimal 41 → 1 byte.
- é is U+00E9 → hexadecimal c3 a9 in UTF-8 → 2 bytes.
- 🙂 is U+1F642 → hexadecimal f0 9f 99 82 in UTF-8 → 4 bytes.
RESULT
Same visible-character count (one) can require 1, 2, or 4 bytes. JavaScript string length may differ again because it counts UTF-16 code units.
START HERE
Starter material
PREREQUISITESA POSIX shell with wc, od, chmod, printf, and file (macOS, Linux, or WSL). iconv is optional for the decoder check.
ONE-TIME SETUPmkdir bytes-encoding-lab && cd bytes-encoding-lab
Create lab.sh, paste this exact content, then run the command below.
#!/usr/bin/env sh
set -eu
printf 'A\né\n🙂\n' > samples.txt
printf '\377' > invalid.bin
printf 'sample bytes: ' && wc -c < samples.txt
od -An -tx1 -c samples.txt
printf 'invalid bytes: ' && od -An -tx1 invalid.binchmod +x lab.sh && ./lab.shSTOP / CLEANUPNo background process. Keep samples.txt, invalid.bin, and lab.sh as lesson evidence.
DO WITH GUIDANCE
Guided exercise
Measure characters as bytes
- Create samples.txt using supplied command.
- Predict byte count for A, é, 🙂, and three newline bytes before running wc.
- Run wc -c and od. Label which hex bytes belong to each character and newline.
- Replace é with decomposed e + combining acute accent; compare visible text and bytes.
Concrete guided solution
- Create lab.sh from Starter material, then run chmod +x lab.sh && ./lab.sh; this command creates samples.txt and invalid.bin.
- Predict 10 bytes: A=1, é=2, 🙂=4, and three newline bytes=3.
- Annotate od output as 41 | 0a | c3 a9 | 0a | f0 9f 99 82 | 0a.
- Create decomposed text with printf 'e\314\201\n' > decomposed.txt; verify 65 cc 81 0a.
- Compare with printf 'é\n' > composed.txt; explain why similar rendering does not mean equal bytes.
DO ALONE
Independent exercise
Create and diagnose invalid UTF-8
- Create invalid.bin containing lone byte ff.
- Inspect it with od -An -tx1 and file invalid.bin.
- Try decoding it with a UTF-8-aware tool available on your machine; capture rejection or replacement behavior.
- Repair file by replacing invalid byte with UTF-8 bytes for é, then prove decoder accepts it.
Concrete independent solution
- Create the invalid byte reproducibly with printf '\377' > invalid.bin.
- Run od -An -tx1 invalid.bin; the only byte must be ff.
- Run file invalid.bin and, where available, iconv -f UTF-8 -t UTF-8 invalid.bin; record rejection, exit status, and stderr.
- Repair with printf '\303\251' > invalid.bin; od must now show c3 a9.
- Rerun the decoder and prove it exits successfully and displays é. Keep before/after bytes in the notebook.
COMPARE
Expected result
- wc -c reports 10: A=1, é=2, 🙂=4, plus three newline bytes.
- Hex sequence is 41 0a c3 a9 0a f0 9f 99 82 0a.
- invalid.bin contains ff, which cannot begin a valid UTF-8 code point.
PROVE
Acceptance checks
Lesson is complete only when every check is true. Each check is stored locally and travels with your JSON backup.
0/5 complete · saved on this device
UNSTICK
Hints
Reveal hints
- Newline is byte 0a and counts even though it is mostly invisible.
- Composed é is c3 a9; decomposed e plus U+0301 begins 65 cc 81.
- Byte ff is never valid in UTF-8.
VERIFY
Solution
Attempt both exercises before opening reference approach.
Reveal reference solution
- Run starter: it creates valid samples and one invalid byte without requiring an editor encoding setting.
- Annotate 41 | 0a | c3 a9 | 0a | f0 9f 99 82 | 0a, totaling 10 bytes.
- Create decomposed sample with printf 'e\314\201\n'; od shows 65 cc 81 0a while screen still resembles é.
- Replace invalid.bin with printf '\303\251' > invalid.bin; od shows c3 a9 and UTF-8 decoding succeeds.
PREDICT · INSPECT · BREAK · DEBUG · MEASURE
Interrogate reality
Prediction: write expected output, state transition, ordering, and failure evidence before running either exercise.
Inspection: Use the shell, process table, filesystem metadata, curl, and Git plumbing. Never trust a command you cannot observe.
Measurement: Count bytes, processes, descriptors, syscalls, round trips, and elapsed time before explaining performance.
Capture raw evidence before explaining.
Change one assumption and force controlled failure.
Find cause with Ghostty · tmux · hx · shell · curl · Git before editing fix.
MASTERY + FRONTIER + BOUNDARY
Own the knowledge
Explain bytes, text, and encodings at beginner, intermediate, and senior depth.
Recreate smallest useful example from blank file without notes or AI.
Schedule recall for day 1, 7, 30, and 90.
Creative frontier lab
Try first without opening the solutions. The constraints invite invention; the reference gives one concrete direction, never the only valid answer.
Create a tiny report comparing bytes, UTF-16 code units, code points, and grapheme clusters for five strings.
CONSTRAINTInclude A, é, decomposed é, 🙂, and a family emoji joined by zero-width joiners.
ORIGINAL IDEARender every measurement beside an escaped code-point sequence so the tool explains surprising counts instead of hiding them.
Reveal frontier solution
- Use TextEncoder().encode(text).length for UTF-8 bytes.
- Use text.length for UTF-16 code units and [...text].length for code points.
- Use Intl.Segmenter with granularity 'grapheme' for user-perceived clusters.
- Print code points with [...text].map(c => `U+${c.codePointAt(0).toString(16).toUpperCase()}`). The family emoji demonstrates many code points but one grapheme.
Design a text format that lets another student reconstruct exact bytes even when the text is invalid UTF-8.
CONSTRAINTIt must be human-inspectable, diffable, and round-trip 00, newline, ff, and valid multibyte UTF-8.
ORIGINAL IDEAPair a hexadecimal payload with a deliberately non-authoritative preview; the preview can fail without corrupting truth.
Reveal frontier solution
- Store one lowercase two-digit hex byte per token, with an optional # preview comment.
- Encode with od -An -v -tx1 and normalize whitespace; decode with xxd -r -p where available.
- Round-trip 00 0a ff c3 a9, then compare original and reconstructed files with cmp.
- Document that the hex payload is authoritative and the preview is replaceable UI.
Make a lookup that appears correct visually but fails because composed and decomposed keys differ.
CONSTRAINTFirst demonstrate the bug; then offer both a byte-preserving policy and a normalized-search policy.
ORIGINAL IDEAStore original bytes for fidelity while deriving a normalized comparison key—two representations with explicitly different jobs.
Reveal frontier solution
- Create a Map keyed by composed é; show lookup with decomposed e + combining acute returns undefined.
- Log both UTF-8 byte sequences to prove cause.
- For search, derive key with text.normalize('NFC') before insertion and lookup.
- Keep original input separately when byte fidelity matters; explain why unconditional normalization may be data loss.
Capability frontier
Push bytes, text, and encodings until another layer becomes justified. Record one robust technique, one contextual trade-off, and one labeled hack or historical curiosity.
CORE · PRACTICAL · CONTEXTUAL · HACK · FRAGILE · HISTORICAL · GOLF
Boundary
Shell composition becomes costly when state, error recovery, or data structure complexity dominates the pipeline.
If this vanished tomorrow…
Reproduce the useful behavior with files, text streams, process primitives, and a minimal compiled or interpreted program.
Why next layer is earned
Networking and HTTP are earned when local processes need to exchange representations across a boundary.