A name badge is a fine first string exercise, right up until a second person joins.
The script worked. It read a name, printed the first name, the last name, and the initials, and the output was exactly what the person who wrote it expected. The next name on the list was longer, and the badge came out as Alexa, Der Hamilton, A. D.. Three lines, none of them right, and no error to explain any of it.
Key takeaways
raw[0:5]means five characters from the left, not the first word — slices never check what they cut..split()with no argument splits on any run of whitespace and handles the padding, which is why.strip()is less necessary than it looks.- A string method returns a new string:
s.upper()on its own changes nothing until you assign it. - Slice when the position is the point, split when the word is the point, and use a method before writing either.
s[0]ands[-1]are safe at any length;s[0:5]is a guess about the data.
Two files, one command
Same contract as the rest of this series: you write the program, the article hands you a checker, you run it until it goes from red to green. No repository, no Docker, python3 only.
Create badge.py:
# badge.py
import sys
raw = sys.stdin.read().strip()
first = raw[0:5].title()
last = raw[6:].title()
print(first)
print(last)
print(f"{first[0]}. {last[0]}.")
Then check_badge.py next to it. It feeds in a name and reads the three lines back:
# check_badge.py
import subprocess
import sys
NAME = " alexander hamilton "
EXPECTED = ["Alexander", "Hamilton", "A. H."]
def report(results):
for label, passed, detail in results:
mark = "PASS" if passed else "FAIL"
note = f" <- {detail}" if detail and not passed else ""
print(f"{mark} {label}{note}")
passed = [ok for _, ok, _ in results].count(True)
print(f"\n{passed} of {len(results)} checks passed")
def main():
run = subprocess.run([sys.executable, "badge.py"], input=NAME, text=True, capture_output=True)
crashed = run.returncode != 0
last_error = run.stderr.strip().splitlines()[-1] if run.stderr.strip() else ""
results = [("the script runs without crashing", not crashed, last_error)]
if crashed:
report(results)
return 1
lines = [line.strip() for line in run.stdout.splitlines() if line.strip()]
results.append(("prints three lines", len(lines) == 3, f"got {len(lines)}: {lines}"))
for index, expected in enumerate(EXPECTED):
got = lines[index] if index < len(lines) else "nothing"
results.append((f"line {index + 1} is {expected!r}", got == expected, repr(got)))
report(results)
return 0 if all(ok for _, ok, _ in results) else 1
if __name__ == "__main__":
sys.exit(main())
$ python3 check_badge.py
PASS the script runs without crashing
PASS prints three lines
FAIL line 1 is 'Alexander' <- 'Alexa'
FAIL line 2 is 'Hamilton' <- 'Der Hamilton'
FAIL line 3 is 'A. H.' <- 'A. D.'
2 of 5 checks passed
Feed it daffa izzuddin instead and all three lines are correct. That is the whole problem: the script is not broken, it is fitted to one piece of data.
A slice counts characters, not words
Square brackets address positions. raw[0] is the first character, raw[-1] is the last, raw[1:4] is positions 1, 2 and 3 (the end is excluded), and raw[3:] runs to the end of the string. That is all a slice is: a range of positions.
So raw[0:5] asks for five characters from the left. When the name was daffa izzuddin, five characters happened to be daffa, the first name, and raw[6:] happened to start right after the space. Both numbers were correct once, for one name, and never again.
Nothing in the expression says "first name". A slice cannot tell a word from a coincidence, and it will not warn you when the data changes shape. That is not a flaw in slicing; it is slicing doing exactly what it was asked.
The methods that know about words
Strings come with methods that already understand the structure you are after, and they are usually shorter than the slice you were about to write. .split() with no argument cuts on any run of spaces, tabs or newlines, and it drops the runs entirely, which is why the padding around the input never reaches the output. .strip() removes whitespace at the two ends. .title() capitalises each word, .upper() and .lower() change case, .replace("a", "b") swaps text, .startswith() and .endswith() answer about the edges, and " ".join(words) is the reverse of split.
The rule that keeps this simple: if the question is "which word", split. If the question is "which character", slice. Nine times out of ten you were asking the first one.
A method does not change the string
This one costs everybody an afternoon. Strings cannot be modified in place:
name = "daffa"
name.upper()
print(name)
daffa
.upper() returns a new string and leaves the old one alone. Nothing was assigned, so the result was dropped. .strip(), .title() and .replace() behave the same way. That is also why the reading line in the script starts with raw = sys.stdin.read().strip(): the assignment is doing the work, not the method.
The fix
# badge.py
import sys
name = sys.stdin.read().strip()
parts = name.split()
first = parts[0].title()
last = parts[-1].title()
print(first)
print(last)
print(f"{first[0]}. {last[0]}.")
$ python3 check_badge.py
PASS the script runs without crashing
PASS prints three lines
PASS line 1 is 'Alexander'
PASS line 2 is 'Hamilton'
PASS line 3 is 'A. H.'
5 of 5 checks passed
split() returns a list of words of whatever length the name happens to be. parts[0] is the first word and parts[-1] is the last one, and both of those are true for any number of words, which is the property the hardcoded slice was pretending to have.
When a slice is the right tool
Slicing is not the villain. It is the right tool when the position itself carries the meaning: the first letter of a word, the last segment of a file name, the first four digits of a year, a fixed-width code from a form. Notice that first[0] survives in the fixed version, because "the first character of this word" is true regardless of how long the word is.
The useful test is whether the number would still be correct if the data doubled in size. first[0] would. raw[0:5] would not.
Before the next article
Give it a single-word name. It does not crash, does not warn, and prints Cher, Cher, C. C. — a badge nobody would accept, produced silently. Then try first, last = name.split() instead of the two indexed lines: with a middle name it raises ValueError: too many values to unpack (expected 2, got 3).
Two failures with nothing in common except that the checker only knows what you told it to expect. Deciding what a one-word badge should say is your job; the checker can only hold you to your own answer.