October 28, 2026•6 min read

Python Strings: the Badge That Only Worked for One Name

The badge script printed the right name for the person who wrote it and the wrong name for everybody else. Slicing counts characters, and a name is words.

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] and s[-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:

PYTHON
# 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:

PYTHON
# 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())
TEXT
$ 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:

PYTHON
name = "daffa"
name.upper()
print(name)
TEXT
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

PYTHON
# 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]}.")
TEXT
$ 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.

Get better at shipping code.

Join the other developers getting tech updates, tips, and relaxed tutorials in their inbox every week.

Stay in the loop

Tech updates, coding tips, and relaxed insights straight to your inbox every week.

No spam. Unsubscribe anytime.

Read next

See all

© 2026 Inva.dev.