Coding Interview Patterns Module 3 – Arrays and hashing
Encoding lists of strings safely
Turn any list of strings into one string and back without loss: why a separator breaks, length-prefix framing, escaping, and counting in the same unit.
What you will learn
- Serialise a list of arbitrary strings into one string that decodes exactly
- Compare length-prefix framing with escaping by size, speed and robustness
- Explain why the encoder and the decoder must count lengths in the same unit
- Test a codec with empty strings, separator characters and non-ASCII text
Before you start
What you’ll learn · Before you start
What you will learn
- Serialise a list of arbitrary strings into one string that decodes exactly
- Compare length-prefix framing with escaping by size, speed and robustness
- Explain why the encoder and the decoder must count lengths in the same unit
- Test a codec with empty strings, separator characters and non-ASCII text
Before you start
On this page
A list of strings is encoded safely when it becomes one string from which a decoder gets back exactly the same list,
for every list there is: joined with commas, ["a,b", "c"] and ["a", "b,c"] both become a,b,c, and no decoder
can tell them apart. To encode and decode strings safely, the decoder must know where each string ends without
trusting what the strings contain. Two methods give it that. length-prefix framing: Writing each item's length in front of it, as in 5#hello, so that a reader knows where the item ends without looking at what it contains. writes each string’s length in
front of it, as in 5#hello, so the decoder knows how much to take without looking inside. Escaping marks every
separator that occurs inside a string, so that only an unmarked separator can end one. Both encode and decode strings
of any content in one left-to-right pass.
The problem: a shopping list in one string
The problem
A shopping-list app keeps its list in the browser’s local storage, which stores only strings, so the list has to
become one string and come back later. Write an encoder that turns any list of strings into one string, and a
decoder that turns it back into exactly the same list: the same strings in the same order, empty ones included. A
string may contain any character: commas, #, digits, quotes, line breaks, emoji, any script. No library
serialiser.
Constraints: up to 10⁴ strings and 10⁵ characters in all; both directions in time linear in the total length.
["milk", "eggs, 12", "#2 pencils"]: a comma and a#inside strings.[]and[""]: no strings, and one empty string, must encode differently.["a,b", "c"]and["a", "b,c"]must encode differently too.["दूध", "🥛"]: milk in Hindi, then the glass-of-milk emoji.
Two lists, one joined string, and a third list back
Text description of the diagram
At the top are two different lists: the first holds the strings "a,b" and "c", the second holds "a" and "b,c". Joining either list with a comma gives the same string, a,b,c, shown once in the middle with an arrow from each list.
Splitting a,b,c on its commas, shown at the bottom, gives a third list, "a", "b" and "c", in a dashed box: neither of the two lists the string came from comes back.
The obvious way: join on a comma, split on a comma
Join the strings with a comma, and split on commas to decode. Each direction is one pass, and the round trip breaks:
"""Joining a list on a comma and splitting it again: where the round trip breaks."""
import random
def encode(items):
return ",".join(items)
def decode(text):
return text.split(",")
for items in (["milk", "bread"], ["eggs, 12", "#2 pencils"], ["a,b", "c"], ["a", "b,c"], [], [""]):
text = encode(items)
back = decode(text)
print(f"{items!r:26} -> {text!r:22} -> {back!r:26} {'same' if back == items else 'DIFFERENT'}")
rng = random.Random(9)
lists = [["".join(rng.choice("ab,#") for _ in range(rng.randint(0, 3))) for _ in range(rng.randint(0, 4))] for _ in range(1_000)]
wrong = [items for items in lists if decode(encode(items)) != items]
with_comma = sum(any("," in s for s in items) for items in wrong)
empty = sum(items == [] for items in wrong)
print(f"1,000 random lists of up to 4 strings made of a, b, ',' and '#': {len(wrong)} come back different")
print(f" {with_comma} of them have a comma inside a string, {empty} are the empty list, {len(wrong) - with_comma - empty} are neither") Output
['milk', 'bread'] -> 'milk,bread' -> ['milk', 'bread'] same ['eggs, 12', '#2 pencils'] -> 'eggs, 12,#2 pencils' -> ['eggs', ' 12', '#2 pencils'] DIFFERENT ['a,b', 'c'] -> 'a,b,c' -> ['a', 'b', 'c'] DIFFERENT ['a', 'b,c'] -> 'a,b,c' -> ['a', 'b', 'c'] DIFFERENT [] -> '' -> [''] DIFFERENT [''] -> '' -> [''] same 1,000 random lists of up to 4 strings made of a, b, ',' and '#': 651 come back different 418 of them have a comma inside a string, 233 are the empty list, 0 are neither
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 join_split.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The lists ["a,b", "c"] and ["a", "b,c"] both become a,b,c, so no decoder can tell which was meant: the
information is gone. The empty list and the list of one empty string both become the empty string. Of 1,000 random
lists, 651 came back different, each because a string held a comma or the list was empty. A rarer separator, such as
| or an invisible control character, only makes the failure rarer: it is still a character a string may contain.
Step through the four lists of the figure:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Joining ["a,b", "c"]. Joining puts a comma between the strings, so ["a,b", "c"] becomes a,b,c, with the comma that was inside the first string next to the one between them.
- Joining ["a", "b,c"]. The second list joins to the same text, a,b,c. Two different lists now share one encoding.
- Splitting it back. Splitting a,b,c on its commas gives ["a", "b", "c"], a third list. No decoder can tell which list was meant: the information is gone.
- The two empty cases. The empty list joins to the empty text, and so does the list of one empty string.
- Splitting the empty text. Splitting the empty text gives [""], one empty string, so the empty list comes back as a list of one string.
- What went wrong. A comma inside a string looks exactly like a comma between strings, so the decoder cannot find the ends by searching the text.
What to notice
A separator inside a string looks exactly like a separator between strings, as the figure shows, so the decoder must not find the ends by searching the content. Either it learns each string’s length before the string starts, or every string is rewritten so that it cannot contain an unmarked separator. A length then raises one more question, which unit it counts, because Python, JavaScript and Go measure the same text differently.
Checkpoint
Check: Any list of strings, in one string and back · Why joining on a separator breaks · Finding each end without searching the content
The practice below starts with whatever you know least.
Practice that adapts to you
Practise this lesson
9 questions about 10 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Length-prefix framing, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- A dry run
- Why the decoder always gets the list back
- The same codec in 14 languages
- When the two sides count differently
- Escaping, the other way, and how the two compare
- The decoder that copies the whole wire, again and again
- Edge cases, tests and variants
- Edge cases and the tests that catch them
- Variants
- Practice: a codec of your own
- A codec that survives any list of strings
- Interview questions
- Why is joining strings with a separator not a safe way to encode a list of strings, even with a rare separator?
- A Python service sends lists of strings to a JavaScript web client and a Go worker as one text field. The strings are user input in any language. Define the format precisely enough that all three decode every list the same way, and say how you would show that they agree.
- Your length-prefixed decoder now reads files that users upload. What can a malformed or hostile input do to it, and how do you make the decoder safe without slowing down valid input?
- You must store 10 million short strings, about 12 bytes each, in one file and later read any single string by its position quickly. Compare length-prefix framing, escaping, and framing with an index of offsets, by file size and by the cost of reading string number 7,000,000.
- Key takeaways
2 diagrams · 6 runnable examples (Python) · 1 exercise with sample tests · 3 interview answers · about 47 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- escape character
- A character that changes the meaning of the character after it, such as a backslash written before a comma to make the comma part of the text instead of a separator.
- length-prefix framing
- Writing each item's length in front of it, as in 5#hello, so that a reader knows where the item ends without looking at what it contains.
References
- RFC 3629: UTF-8, a transformation format of ISO 10646 (IETF (RFC Editor))
- Built-in Types: text sequence type (str) (Python Software Foundation)
- ECMAScript language specification: the String type (Ecma International (TC39))
- Java SE 25 API: String.length() (Oracle)
- The Go Programming Language Specification: string types (The Go Authors)
- Web Storage (HTML Living Standard): the Storage interface (WHATWG)
- Netstrings (D. J. Bernstein) (D. J. Bernstein)
- RFC 9112: HTTP/1.1, section 7.1 chunked transfer coding (IETF (RFC Editor))
- Protocol Buffers documentation: encoding (length-delimited records) (Google)
- RFC 4180: common format and MIME type for CSV files (IETF (RFC Editor))
- RFC 8259: the JSON data interchange format, section 7 strings (IETF (RFC Editor))
- Strings: the String type, length and grapheme clusters (The Swift Programming Language) (Swift.org)
- Rust standard library: str (len, slicing at character boundaries) (The Rust Project)
- String class (.NET API): Length counts UTF-16 code units (Microsoft)
- String class (Dart core library) (Dart (Google))
- PHP manual: strlen (returns the number of bytes) (The PHP Group)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress