Python105 min total · 18 parts
Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas
Part 3 of 18 · ~3 min
Strings and String Formatting
Every one of those LogEntry fields started life as a slice of one long line, so before there's any collection to put things in, there's a string to take apart.
raw = "2026-03-02T09:14:03Z 198.51.100.7 POST /api/login 401 42"
raw[0] = "1" # TypeError: 'str' object does not support item assignment
Strings are immutable — you never edit one in place, you always produce a new one. .split() doesn't cut raw into pieces so much as read it once and hand back six brand-new strings that happen to share none of its memory.
Building an alert summary by repeatedly appending to a string runs straight into that immutability, one offender at a time:
# Slow — offenders 1..n each recreate the ENTIRE string built so far, then add one more line
summary = ""
for ip, count in offenders:
summary += f"{ip}: {count} requests\n"
# Fast — every piece is built once, joined together in a single pass
summary = "\n".join(f"{ip}: {count} requests" for ip, count in offenders)
A watcher flagging three IPs a day will never notice. A watcher that eventually flags three thousand across a busy incident will, and the cost will be this loop — an O(n) report turned into O(n²) — not anything to do with the log itself.
f-strings vs. .format() vs. %
ip, path, count = "198.51.100.7", "/api/login", 340
f"{ip} hit {path} {count} times" # "198.51.100.7 hit /api/login 340 times" — preferred
"{} hit {} {} times".format(ip, path, count) # same result, older style
"%s hit %s %d times" % (ip, path, count) # oldest style, still common in logging calls
Reach for f-strings by default — they've been standard since 3.6, and the curly braces hold a genuine expression rather than a placeholder, so something like f"{count * 2}" runs the multiplication right there rather than requiring it be computed first. The % style is the one exception worth keeping around, and only inside logging calls: logger.warning("%s hit %s %d times", ip, path, count) doesn't build that string at all unless a WARNING-level message is actually going to be emitted, so a watcher running at INFO skips the formatting work entirely for a message nobody's about to read.
Common string methods worth having memorized
raw.strip() # trims the trailing newline every line comes with
raw.split() # ['2026-...Z', '198.51.100.7', 'POST', '/api/login', '401', '42']
"401,403,429".split(",") # ['401', '403', '429']
"login" in "/api/login" # True — substring check, used to route by endpoint
"POST".lower() == "post" # case-insensitive method comparison
"/api/login".replace("/api", "") # "/login"
One more thing worth knowing before it bites you: everything above assumes raw is already a str. Open a log file the normal way — open(path) — and Python decodes it using the platform's default encoding as it reads, so you get str back and never think about it again. Open it as open(path, "rb"), which you'll do the day a log needs to be streamed over a socket or hashed byte-for-byte, and every line comes back as bytes instead — b"198.51.100.7 ..." rather than "198.51.100.7 ...". bytes and str don't mix: b"a" + "b" raises TypeError, and .split() on bytes returns bytes. The fix is raw_bytes.decode("utf-8") on the way in, once, rather than sprinkling b"..." literals through code that was written assuming text.