Strings are immutable in Python, Java, Go, and JavaScript, which shapes every mechanical decision in this section — from why naive concatenation in a loop is a performance trap in all four, to how each language actually represents a character internally (Go is UTF-8 bytes; Java/JS are UTF-16 units; Python is code points). This is about the mechanics of building, comparing, and indexing text correctly and efficiently, not about string-matching algorithms or patterns (covered elsewhere in the roadmap).
Language Verdict: Pros, Cons & Recommendation
str is a sequence of Unicode code points, not UTF-16 units — len() and iteration stay correct even for emoji/astral characters- No reference-vs-value split:
== always compares by value, with no new String()-style trap - F-strings compile to efficient bytecode — the fastest and most readable formatting option
"".join(parts) is one obvious idiom for O(n) building, no separate builder class needed
- No dedicated builder class — you must know to accumulate into a list and
join, not += in a loop .lower()/.upper() follow Unicode default case folding, not locale rules — casefold() needed for robust comparisons
StringBuilder is a dedicated, well-understood, unsynchronized builder — the unambiguous right tool for loop concatenationtoLowerCase(Locale)/equalsIgnoreCase() give explicit, mature control over locale-sensitive comparison.codePoints()/Character.toChars() provide a direct, built-in fix for surrogate-pair iteration bugs
char/.length() operate on UTF-16 code units, not code points — surrogate pairs silently split under naive iteration- The string pool plus
new String("x") creates the classic == vs. .equals() trap unique to Java among the three String.format() allocates a Formatter and parses the format string at runtime — slower than plain concatenation
strings.Builder is the dedicated, unambiguous loop-concatenation tool — same role as Java's StringBuilder== compares string bytes by value; no new String() / intern pool trapfor _, r := range s iterates runes (code points), so emoji/astral characters don't split the way Java/JS char indexing does[]byte(s) / string(bytes) conversions are explicit; []byte is the mutable buffer when you need in-place ASCII edits
len(s) is bytes, not runes — len("😀") == 4; use utf8.RuneCountInString or []rune(s) when the problem counts characterss[i] is a byte, not a rune — indexing into the middle of a UTF-8 sequence yields a continuation byte, not a character- Converting
[]byte(s) always copies; slicing a string (s[i:j]) is in bytes and panics if you slice mid-rune only at decode time, not at the slice itself
- Primitive strings always compare by value with
=== — no Java-style reference trap outside new String() wrappers - Template literals are readable and as fast as a single
+ chain for one-off interpolation for...of/spread iterate by code point, a simple built-in fix for common surrogate-pair cases
- No built-in builder class at all — array-push-then-
.join() is the only accumulation idiom - Same UTF-16-code-unit representation as Java (
"\u{1F600}".length === 2), so the surrogate-pair trap applies equally - Full grapheme-cluster correctness (e.g. skin-tone emoji) needs the less-common
Intl.Segmenter, not covered by basic iteration fixes
Recommendation: Python's code-point str and Go's rune-range loop both avoid the Java/JS surrogate-pair bug class; Python remains the default for speed of writing. In Java/JS/Go, always accumulate loop concatenation via StringBuilder / array+.join() / strings.Builder (never +=), and remember Go's len is bytes.
Coding Mechanics, Side by Side
Strings are immutable in all four languages, so s += x inside a loop reallocates and copies the entire string on every iteration — O(n) work done n times, O(n^2) total.
# BAD: O(n^2) -- each += allocates a new string and copies everything so far
s = ""
for word in words:
s += word
# GOOD: O(n) -- join allocates the final buffer once
s = "".join(words)
CPython has a narrow optimization (+= on a string with refcount 1 may resize in place in some cases), but it's an implementation detail you should never rely on for correctness or complexity analysis. "".join(iterable) is the idiomatic O(n) fix — it computes the total length once and allocates a single buffer.
parts = []
for line in lines:
parts.append(process(line))
result = "\n".join(parts)
Python has no separate builder type — the accumulate-into-a-list-then-join pattern is the StringBuilder equivalent, and it's the standard idiom for any loop that builds up text.
a = "hello"
b = "hello"
print(a is b) # True (usually) -- CPython interns identifier-like/short literals
c = "".join(["hel", "lo"])
print(a is c) # False -- runtime-built string, not automatically interned
print(a == c) # True -- always compare strings with ==, never is
CPython automatically interns strings that look like identifiers and small string literals as an implementation-detail optimization — never rely on is for string equality, since it's not guaranteed by the language spec and can vary between builds/versions. Always use ==.
The single most valuable cross-language difference here: Java and JS strings are sequences of UTF-16 code UNITS, Python 3 strings are sequences of Unicode code POINTS, and Go strings are sequences of UTF-8 BYTES (len is bytes; range yields runes).
s = "a\U0001F600b" # "a", then grinning-face emoji, then "b"
print(len(s)) # 3 -- one code point per visible character
for ch in s:
print(ch) # iterates cleanly: 'a', the emoji, 'b'
Python 3 str is a sequence of Unicode code points, not UTF-16 units, so len() and naive character-by-character iteration both do the intuitively correct thing even for astral characters like most emoji. This is a genuine structural advantage over Java/JS for text-processing correctness.
s = "hello world"
sub = s[0:5] # "hello" -- always allocates a new string, O(k)
ch = s[1] # O(1) index access
Slicing always copies into a new string object, O(k) for a slice of length k — Python strings have never shared backing storage between a string and its substrings. Index access is O(1) since strings are stored as a contiguous code-point/byte buffer internally.
a = "hello"
b = "hel" + "lo"
print(a == b) # True -- == always compares by value for str
Python's == compares string value correctly and consistently, regardless of how the strings were constructed — there is no reference-vs-value split to worry about, unlike Java. (is compares identity and should never be used for string equality, per the interning block above.)
Naive lowercase/uppercase conversion can surprise you in all four languages — the canonical trap is the Turkish 'İ'/'i' pair, where the dotless/dotted 'I' doesn't map the way English speakers expect.
print("HELLO".lower() == "hello") # True -- fine for ASCII/most text
print("İstanbul".lower()) # 'i̇stanbul' -- dotted lowercase i + combining dot, len 9 not 8!
str.lower()/str.upper() use Unicode's default case-folding rules, which are locale-independent in Python (there's no locale-aware .lower() variant used by default) — but Unicode's own rules can still surprise you, as the Turkish İ example shows producing an extra combining character. For robust case-insensitive comparisons, prefer str.casefold() over .lower().
name, score = "Ada", 97
s = f"{name} scored {score}%" # f-string: compiled, fast
s2 = "{} scored {}%".format(name, score) # older .format() idiom, slightly slower
F-strings are parsed into efficient bytecode at compile time and are both the most readable and fastest option — prefer them over .format() or manual + concatenation. They're for single-expression formatting, not loop accumulation; for building up text across many iterations, still use "".join(...).