Filter concepts by levelShowing all levels.

Data Structures & Algorithms · Section 4

Strings

Level
beginner
Read
90 min
Concepts
15

Why strings being immutable shapes how they must be built, the classic string algorithms — palindrome and anagram checks, substring-vs-subsequence, naive pattern matching, string reversal, and manual number parsing — and the Python tools (split/join/strip, is-checks, Counter, ord/chr, find) built for text.

What is true here

  1. A string can never be edited in place — every apparent edit builds an entirely new string.
  2. Two pointers closing in from both ends check a palindrome in O(n) time, O(1) space.
  3. Anagram checks need character counts, not just which characters are present.
  4. ord(c) - ord('a') maps a single case's letters to a 0-25 index for a fixed-size frequency array.

What you will be able to do

  • Check whether a string is a palindrome or whether two strings are anagrams, and explain each approach's complexity
  • Distinguish a substring from a subsequence, and implement a check for each
  • Write a naive pattern search and know when str.find() replaces it in real code
  • Use ord()/chr() to map characters to array indices for a fixed-size frequency count

Concepts

The classic string algorithms and the immutability facts that shape how strings must be built.

Strings as immutable sequences (why in-place edits aren't free)

standardbeginner

A Python string cannot be changed in place — s[0] = 'x' raises an error. Any 'edit' actually builds a brand-new string, which is why string-building patterns matter (§2's O(n²) `+=` trap comes directly from this).

Think of it as

A string behaves like a tuple of characters: indexable and iterable, but frozen once created. 'Editing' one character really means constructing an entirely new string that differs by one character — there is no operation that changes a string's existing memory in place.

What we're doing: Confirm that direct item assignment on a string fails, and show the correct way to change one character.

strings_immutable.pypython
s = "cat"
try:
    s[0] = "b"
except TypeError as e:
    print(type(e).__name__, e)

s = "b" + s[1:]
print(s)
3
Item assignment is simply not defined for str — this raises immediately, rather than silently doing nothing.
Output
TypeError 'str' object does not support item assignment
bat

Why this works: The TypeError confirms strings have no in-place mutation at all — the only way to 'change' one character is to build a new string (here, the first character replaced, the rest sliced and reused) and rebind the name to it.

Remember: A string can never be edited in place — every apparent edit builds an entirely new string, which is the root cause of §2's O(n²) `+=`-in-a-loop trap.

See also: string concat o n squared

Building strings efficiently (join vs += in a loop)

standardbeginner

Collect pieces in a list and call ''.join(pieces) once, rather than accumulating with += inside a loop — join is the practical fix; §2 covers exactly why += is O(n²).

Think of it as

Think of building a string like assembling a sentence: writing each word onto a growing sign one at a time (+=) means re-painting the whole sign every time; join() is more like laying all the words down at once, in their final positions, in a single pass.

python
words = ["Data", "Structures", "and", "Algorithms"]
sentence = " ".join(words)

What we're doing: Join a list of words into a sentence, and format a single value with an f-string.

building_strings.pypython
words = ["Data", "Structures", "and", "Algorithms"]
print(" ".join(words), f"{words[0]} {words[1]}")
2
" ".join(words) builds the whole sentence in one pass; the f-string handles a one-off, single piece of formatted text — a different job, not a competing way to do the same thing.
Output
Data Structures and Algorithms Data Structures

Why this works: join() is for combining MANY pieces at once; an f-string is for formatting one specific piece of output — reaching for the right one depends on whether you have a collection to combine or a single value to display.

  • Add StringseasyLeetCode

    Append each computed digit to a list, then "".join(reversed(digits)) once at the end — never += a growing result string inside the loop.

  • Reverse Words in a StringmediumLeetCode

    " ".join(words) builds the final sentence in one call — the practical habit this concept teaches, applied to a real problem.

Remember: Collect pieces in a list and ''.join() once — never accumulate a string with += inside a loop.

See also: string concat o n squared

Palindrome checks

corebeginner

A palindrome reads the same forwards and backwards. Two pointers walking inward from both ends, comparing as they go, check this in O(n) without ever building a reversed copy.

Think of it as

Two pointers start at opposite ends and walk toward the middle, comparing the characters they point at each step. Any mismatch proves it is not a palindrome immediately; if the pointers meet or cross without ever disagreeing, every pair matched, which is exactly what 'reads the same both ways' means.

What we're doing: Check whether a word is a palindrome using two pointers, without building a reversed copy.

palindrome_checks.pypython
def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True


print(is_palindrome("racecar"), is_palindrome("hello"))
1
left starts at the first character, right at the last — they walk toward each other.
4
The moment any pair disagrees, the answer is already False — no need to keep comparing the rest.
Output
True False

Why this works: "racecar" matches at every pair of positions as the pointers close in, confirming it reads the same both ways; "hello" fails on the very first comparison (h vs o), returning False immediately without checking the rest.

Checking a phrase palindrome without normalizing case and punctuation

Wrong

python
def is_palindrome_naive(s):
    return s == s[::-1]


print(is_palindrome_naive("A man a plan a canal Panama"))

Better

python
def is_palindrome_normalized(s):
    cleaned = "".join(c.lower() for c in s if c.isalnum())
    return cleaned == cleaned[::-1]


print(is_palindrome_normalized("A man a plan a canal Panama"))

What you see: is_palindrome_naive("A man a plan a canal Panama") returns False, even though the phrase IS a palindrome once case and spaces are ignored.

Why: A direct s == s[::-1] compares the RAW characters, including case ('A' vs 'a') and spaces — which almost never match up symmetrically in a real sentence. Filtering to only alphanumeric characters and lowercasing them first is what makes a phrase-level palindrome check actually work.

  • Valid PalindromeeasyLeetCode

    This is the phrase-normalization mistake fixed above — filter to alphanumeric characters, lowercase them, then two-pointer check.

  • A step up: instead of checking one whole string, expand outward from every possible center and keep the longest palindrome found.

Remember: Two pointers closing in from both ends check a palindrome in O(n) time, O(1) space — and a real sentence needs its case and punctuation stripped first.

See also: consider edge cases

Anagram checks (via sorting or frequency count)

corebeginner

Two strings are anagrams if they contain exactly the same characters, the same number of times each. Sorting both and comparing (O(n log n)), or comparing character counts with Counter (O(n)), both work — Counter is faster.

Think of it as

Two ways to answer 'same letters, same counts?': make the order irrelevant by sorting both strings the same way and comparing (if they're anagrams, the sorted versions are identical), or count every character in each and compare the counts directly. Comparing SETS of characters is not enough — a set throws away exactly the count information an anagram check needs.

What we're doing: Check two pairs of words for being anagrams, both by sorting and by frequency count.

anagram_checks.pypython
def is_anagram_sorted(a, b):
    return sorted(a) == sorted(b)


def is_anagram_counter(a, b):
    from collections import Counter
    return Counter(a) == Counter(b)


print(is_anagram_sorted("listen", "silent"), is_anagram_counter("listen", "silent"))
print(is_anagram_counter("cat", "dog"))
1
Sorting both strings the same way makes anagrams compare as literally equal lists of characters.
5
Counter builds a {character: count} mapping for each string — two anagrams always produce identical mappings.
Output
True True
False

Why this works: "listen" and "silent" share the exact same six letters, so both methods agree they are anagrams; "cat" and "dog" share no letters at all, so both correctly report False.

Using set() instead of Counter() or sorted()

Wrong

python
def is_anagram_set_buggy(a, b):
    return set(a) == set(b)


print(is_anagram_set_buggy("aabb", "ab"))

Better

python
from collections import Counter

def is_anagram_counter(a, b):
    return Counter(a) == Counter(b)


print(is_anagram_counter("aabb", "ab"))

What you see: is_anagram_set_buggy("aabb", "ab") returns True, even though "aabb" (two a's, two b's) and "ab" (one of each) are clearly not anagrams of each other.

Why: A set collapses repeated characters down to one — set('aabb') and set('ab') are both {'a', 'b'}, so the DUPLICATE-COUNT information an anagram check actually needs is thrown away before the comparison even happens. Counter (or sorted, which preserves every repeat) is what keeps that information intact.

  • Valid AnagrameasyLeetCode

    This is the exact concept taught above — Counter(s) == Counter(t), or sort both and compare.

  • Group AnagramsmediumLeetCode

    Use each word's sorted-letters (or Counter) as a hash-map key — anagrams collide onto the same key and land in the same group.

Remember: Anagram checks need character COUNTS, not just which characters appear — Counter(a) == Counter(b) or sorted(a) == sorted(b), never set(a) == set(b).

See also: character frequency counting

Substring vs subsequence (distinction matters a lot)

standardbeginner

A substring is a CONTIGUOUS run of characters from the original string. A subsequence keeps the original order but can skip characters — "ace" is a subsequence of "abcde" but not a substring of it.

Think of it as

A substring is what you get by cutting the string at two points and keeping what's between — nothing removed from the middle. A subsequence is what you get by deleting zero or more characters and keeping the rest in their original order — the characters do not have to be next to each other.

What we're doing: Confirm "abc" is found as a substring, and that "ace" is a subsequence of "abcde" (but "aec" is not).

substring_vs_subsequence.pypython
def is_subsequence(s, t):
    it = iter(t)
    return all(c in it for c in s)


print("abc" in "xabcy")
print(is_subsequence("ace", "abcde"), is_subsequence("aec", "abcde"))
1
it is an iterator over t — "c in it" advances it forward until c is found (or the iterator runs out), which is exactly the "skip characters, keep order" rule a subsequence needs.
6
"abc" in "xabcy" checks for a CONTIGUOUS match — Python's built-in `in` on strings is a substring check.
Output
True
True False

Why this works: "abc" sits together, unbroken, inside "xabcy" — a true substring match. "ace" can be found by skipping "b" and "d" while keeping a, c, e in order — a valid subsequence — but "aec" cannot, since e appears before c in the source, breaking the required order.

  • Is SubsequenceeasyLeetCode

    This is the exact concept taught above — walk t with an iterator, advancing it once per matched character of s.

  • A step up in difficulty: instead of checking IF one string is a subsequence of another, find the length of the longest sequence common to both, via dynamic programming.

Remember: A substring must be contiguous; a subsequence only needs to preserve order — every substring is a subsequence, but not the other way around.

Character frequency counting

standardbeginner

Counting how many times each character appears in a string is the basis of anagram checks, first-unique-character problems, and more — a dict built manually does the job in O(n), and collections.Counter does the same thing in one call.

Think of it as

Walk the string once, and for each character either start its count at 1 (first time seen) or add 1 to its existing count. That single pass, one dict lookup-and-update per character, is the entire algorithm — Counter is the same idea, just already written.

python
from collections import Counter

freq = Counter("banana")   # Counter({'a': 3, 'n': 2, 'b': 1})

What we're doing: Build a character frequency count by hand, and confirm it matches Counter's result.

char_frequency.pypython
from collections import Counter


def char_frequency(s):
    freq = {}
    for c in s:
        freq[c] = freq.get(c, 0) + 1
    return freq


print(char_frequency("banana"))
print(dict(Counter("banana")))
3
freq.get(c, 0) returns 0 for a character seen for the first time, so + 1 correctly starts its count at 1 — no separate "have I seen this before" check needed.
Output
{'b': 1, 'a': 3, 'n': 2}
{'b': 1, 'a': 3, 'n': 2}

Why this works: Both the manual dict and Counter agree exactly — 1 "b", 3 "a"s, 2 "n"s — confirming Counter is doing the identical single-pass counting a hand-written version would.

  • Valid AnagrameasyLeetCode

    Build a frequency count of each string and compare them — two anagrams always produce identical counts.

  • Count every character's frequency first, then output characters most-frequent first — the count is the whole problem, the sort is just presenting it.

Remember: A frequency count is one pass with dict.get(key, default) — Counter(s) is the same thing, already written, plus lookup helpers like .most_common(n).

See also: anagram checks

String reversal (whole string, words in a sentence)

standardbeginner

s[::-1] reverses a whole string's characters. Reversing the WORDS in a sentence (while keeping each word spelled forward) is a different job: split into words, reverse that list, and join it back.

Think of it as

Reversing characters and reversing word order are not the same operation, and mixing them up is a common early confusion — s[::-1] on "the quick fox" gives "xof kciuq eht" (every character flipped, words spelled backward too), while reversing WORD order needs the string split into pieces first, so each piece stays spelled forward.

What we're doing: Reverse a whole string's characters, and separately reverse the order of words in a sentence.

string_reversal.pypython
def reverse_words(sentence):
    return " ".join(reversed(sentence.split()))


print("hello"[::-1])
print(reverse_words("the quick brown fox"))
1
sentence.split() breaks the sentence into a list of words; reversed() flips their ORDER without touching the spelling of any single word.
2
" ".join(...) puts the reversed-order words back together with single spaces.
Output
olleh
fox brown quick the

Why this works: "hello"[::-1] flips every character, giving "olleh". reverse_words keeps every word spelled correctly ("fox", "brown", "quick", "the") while reversing their ORDER — a genuinely different transformation from character reversal.

  • Reverse StringeasyLeetCode

    The character-reversal half of this concept — two pointers swapping from both ends inward, in place.

  • Reverse Words in a StringmediumLeetCode

    The word-order half — split on whitespace, reverse the list of words, and join back with single spaces, handling extra/leading/trailing spaces.

Remember: s[::-1] reverses characters; reversing WORD order needs split(), reversed(), and join() — do not confuse the two.

Basic pattern matching (naive substring search)

coreintermediate

The naive way to find a pattern inside text: try matching it starting at every possible position, comparing character by character, and stop at the first full match. O(n * m) worst case, where n is the text length and m the pattern length.

Think of it as

Slide the pattern across the text one position at a time, like a stencil. At each position, compare the stencil against what is underneath, character by character. The moment a full comparison succeeds, that starting position is the answer; if the stencil reaches the last position where it could still fit without a match, the pattern is not present.

What we're doing: Find the starting index of a pattern inside a text by trying every possible position.

naive_search.pypython
def naive_search(text, pattern):
    n, m = len(text), len(pattern)
    for i in range(n - m + 1):
        if text[i:i + m] == pattern:
            return i
    return -1


print(naive_search("hello world", "world"))
print(naive_search("hello world", "xyz"))
3
range(n - m + 1) is every starting index where an m-length pattern could still fully fit inside an n-length text — the +1 matters, since the pattern can legally start at index n - m itself.
4
A slice comparison checks the whole pattern against that starting position in one expression.
Output
6
-1

Why this works: "world" starts at index 6 in "hello world" — the first (and only) position where the 5-character slice matches exactly. "xyz" never matches any position, so every comparison fails and -1 is correctly returned.

Off-by-one: stopping one position too early

Wrong

python
def naive_search_buggy(text, pattern):
    n, m = len(text), len(pattern)
    for i in range(n - m):        # bug: misses the last valid start index
        if text[i:i + m] == pattern:
            return i
    return -1


print(naive_search_buggy("abcde", "de"))

Better

python
def naive_search_fixed(text, pattern):
    n, m = len(text), len(pattern)
    for i in range(n - m + 1):
        if text[i:i + m] == pattern:
            return i
    return -1


print(naive_search_fixed("abcde", "de"))

What you see: naive_search_buggy("abcde", "de") returns -1, even though "de" clearly appears at index 3 of "abcde".

Why: range(n - m) stops one index too early — for text="abcde" (n=5) and pattern="de" (m=2), n - m = 3, and range(3) only reaches index 2, never trying index 3, the pattern's actual (and only) valid starting position. The +1 is not decorative — range(n - m + 1) is the correct inclusive bound.

  • This is the naive search taught above, as a real problem — implement it by hand (str.find() solves it in one call in real code).

  • A sliding window of the pattern's length, sliding across the text, comparing character frequencies at each position instead of exact characters.

Remember: A pattern of length m can start at any index from 0 to n - m INCLUSIVE — range(n - m + 1), not range(n - m).

See also: analyzing loops

String-to-number and number-to-string parsing without built-ins

standardintermediate

Writing your own int(s) teaches the mechanism: each digit character's value comes from ord(c) - ord('0'), and building the number means value = value * 10 + digit, left to right. Use Python's real int()/str() in production code.

Think of it as

Reading a number left to right, each new digit shifts everything already read one place value to the left (multiply by 10) before adding the new digit in — exactly like how a person reads '482' as 4, then 4*10+8=48, then 48*10+2=482. A leading sign character is handled separately, before the digit loop.

What we're doing: Parse a signed numeric string into an integer by hand, and confirm it matches int().

manual_parsing.pypython
def str_to_int(s):
    sign = 1
    i = 0
    if s[0] in "+-":
        sign = -1 if s[0] == "-" else 1
        i = 1
    value = 0
    for c in s[i:]:
        value = value * 10 + (ord(c) - ord("0"))
    return sign * value


print(str_to_int("482"), str_to_int("-17"))
print(int("482"), int("-17"))
7
Each iteration shifts the running total left one place (value * 10) and adds the new digit's value — exactly how reading the digits left to right builds the number.
Output
482 -17
482 -17

Why this works: The manual parser agrees exactly with Python's own int() on both a positive and a negative number, confirming the sign-then-digits mechanism is correct — this is the algorithm int() itself is built on, made visible.

  • Add StringseasyLeetCode

    Digit-by-digit arithmetic from the right, tracking a carry — the same "characters as digit values" mechanism as manual parsing, applied to addition instead of building one number.

  • String to Integer (atoi)mediumLeetCode

    The manual parser taught above, hardened: skip leading whitespace, handle an optional sign, stop at the first non-digit, and clamp to the 32-bit signed range.

Remember: Manual parsing is value = value * 10 + digit, left to right, with the sign handled separately first — but real code should always call int()/str() rather than reimplementing this.

Advertisement

Python tools

The standard-library methods and idioms built specifically for working with text.

`str.split()`, `str.join()`, `str.strip()`

referencebeginner

split() breaks a string into a list of pieces (on whitespace by default, or a given separator). join() does the reverse — pieces back into one string. strip() removes leading/trailing whitespace (or given characters).

python
"  a,b,c  ".strip()          # "a,b,c" — leading/trailing whitespace gone
"a,b,c".split(",")           # ["a", "b", "c"]
"-".join(["a", "b", "c"])    # "a-b-c"
  • A warm-up in thinking about a string as pieces to split, even though the counting solution here does not call .split() itself.

  • Reverse Words in a StringmediumLeetCode

    The idiomatic solution is exactly split() → reverse the list → " ".join() — this concept's three tools used together on one problem.

Remember: split() breaks apart, join() puts back together, strip() trims the edges — the three most common string-shaping tools.

`str.isalpha()`, `str.isdigit()`, `str.isalnum()`

referencebeginner

isalpha() checks every character is a letter, isdigit() checks every character is a digit, isalnum() checks every character is a letter OR digit — all return False on an empty string.

python
"abc".isalpha()      # True
"123".isdigit()      # True
"abc123".isalnum()   # True
"abc123".isalpha()   # False — contains digits

What we're doing: Confirm each check only passes when every character in the string qualifies.

is_checks.pypython
print("abc".isalpha(), "123".isdigit(), "abc123".isalnum(), "abc123".isalpha())
1
isalpha() on "abc123" is False specifically because of the digits — ALL characters must qualify, not just some.
Output
True True True False

Why this works: The first three checks each match strings made entirely of their target character class; the last one shows a mixed string fails a stricter single-class check even though isalnum() would accept it.

  • Valid PalindromeeasyLeetCode

    c.isalnum() is exactly how the string gets filtered down to only letters and digits before the palindrome check.

  • String to Integer (atoi)mediumLeetCode

    c.isdigit() is checked character by character to know exactly where the numeric part of the string ends.

Remember: Each is*() check requires EVERY character to qualify — a mixed string like "abc123" fails isalpha() and isdigit(), but passes isalnum().

`collections.Counter` for frequency maps

standardbeginner

Counter(iterable) builds a dict-like frequency map in one call, and adds extras a plain dict does not have — most_common(n) for the top n items, and arithmetic operators for combining or subtracting counts.

Think of it as

Counter is a dict specialized for exactly one job — counting — so the operations that job needs (most common, add two counts together, subtract one from another) are built in, instead of being written by hand every time character or word frequency comes up.

What we're doing: Build a frequency count with Counter and find the two most common characters.

counter_frequency.pypython
from collections import Counter

print(Counter("mississippi"))
print(Counter("mississippi").most_common(2))
3
most_common(2) sorts by count, descending, and returns the top 2 — no separate sort step needed.
Output
Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
[('i', 4), ('s', 4)]

Why this works: 'i' and 's' both appear 4 times each — the most of any character in "mississippi" — and most_common(2) surfaces exactly those two, in one call, without a manual sort by count.

  • Valid AnagrameasyLeetCode

    Counter(s) == Counter(t) solves this in one line.

  • Top K Frequent ElementsmediumLeetCode

    Counter(nums).most_common(k) gets most of the way there in one call — the follow-up (beat O(n log n)) is where bucket sort comes in.

Remember: Counter is a dict built for counting — reach for .most_common(n) instead of manually sorting a frequency dict by value.

See also: character frequency counting

`ord()` / `chr()` for character-to-index tricks

coreintermediate

ord(c) gives a character's numeric code point; chr(n) does the reverse. ord(c) - ord('a') turns a lowercase letter into a 0-25 index — the trick behind fixed-size, 26-slot frequency arrays instead of a general dict.

Think of it as

Letters 'a' through 'z' have consecutive code points, so subtracting ord('a') shifts 'a' to 0, 'b' to 1, and so on up to 'z' at 25 — a direct, arithmetic mapping from letter to array index, with no lookup needed. This only holds for one consistent case; mixing upper and lower case breaks the arithmetic immediately.

What we're doing: Build a fixed-size, 26-slot letter frequency count using ord() arithmetic instead of a dict.

ord_chr_tricks.pypython
def letter_frequency_array(s):
    counts = [0] * 26
    for c in s:
        counts[ord(c) - ord("a")] += 1
    return counts


counts = letter_frequency_array("banana")
print(counts[0], counts[13])   # counts['a'], counts['n']
3
ord(c) - ord('a') is the entire mapping from letter to array slot — 'a' becomes 0, 'n' becomes 13, with no dict lookup involved.
Output
3 2

Why this works: "banana" has 3 a's (index 0) and 2 n's (index 13) — the counts array agrees exactly, confirming the ord()-based index mapping landed each letter in the correct slot.

Assuming ord(c) - ord('a') works for uppercase letters too

Wrong

python
def letter_index_buggy(c):
    return ord(c) - ord("a")


print(letter_index_buggy("A"))   # should be a valid index 0-25

Better

python
def letter_index_fixed(c):
    return ord(c.lower()) - ord("a")


print(letter_index_fixed("A"))

What you see: letter_index_buggy('A') returns -32 — a negative number, which would crash or silently corrupt a fixed-size array indexed by this value.

Why: 'A' (uppercase) has a lower code point than 'a' (lowercase) in ASCII/Unicode, so ord('A') - ord('a') is negative, not in the intended 0-25 range. The fix normalizes case with .lower() BEFORE the arithmetic, so both 'a' and 'A' map to the same index 0.

  • Isomorphic StringseasyLeetCode

    Map each character to its position/partner using a small array or dict keyed by character — the same character-to-index habit taught above, applied to a mapping instead of a count.

  • Track the last-seen index of each character (an array or dict keyed by character) to jump the window's start forward the moment a repeat is found.

Remember: ord(c) - ord('a') maps a single case's letters to 0-25 — normalize case first with .lower()/.upper(), or mixed-case input silently produces out-of-range or negative indices.

See also: anagram checks

`str.find()` vs manual scanning

standardbeginner

str.find(pattern) does exactly what this section's naive_search does — returns the first matching index, or -1 — but as a C-implemented, highly optimized built-in. Write the manual version to learn the mechanism; use find() in real code.

Think of it as

find() and a hand-written naive_search answer the identical question (where does this pattern first appear, or does it appear at all?) — the difference is entirely in implementation speed, not in what question is being answered. Reaching for the built-in is the same trade every other 'exercise vs practice' concept in this section makes.

What we're doing: Confirm str.find() gives the identical result to this section's hand-written naive_search.

find_vs_manual.pypython
print("hello world".find("world"))
print("hello world".find("xyz"))
1
find() returns the same starting index (6) that a hand-written naive_search over the same text and pattern would.
Output
6
-1

Why this works: find() answers exactly the same question as the manually written naive_search earlier in this section — same found-index-or-negative-one contract — while running far faster in practice.

  • The problem this concept is named after — implement it by hand once, then note that haystack.find(needle) is the one-line real-code answer.

  • find() alone is not enough here — every match needs to be found and checked, which is where a sliding window earns its place over repeated find() calls.

Remember: Write a naive search once to understand the mechanism — then use str.find() (or str.index() when a missing match should raise) in real code.

See also: basic pattern matching

f-strings for building output, not core algorithm logic

referencebeginner

f-strings are for DISPLAYING a result — formatting a message, a log line, a summary. Routing actual algorithm logic through a formatted string (e.g. building a value by concatenating text and re-parsing it) is a sign the logic belongs in real code, not text.

What we're doing: Use an f-string for what it is meant for: displaying a computed result.

fstring_output.pypython
name, score = "Ada", 97
print(f"{name} scored {score}%")
2
The f-string formats an already-computed value for display — it plays no role in how score itself was calculated.
Output
Ada scored 97%

Why this works: name and score were computed elsewhere; the f-string's only job here is presenting them — exactly the boundary this concept is naming.

Remember: f-strings format a result for a human to read — if a solution's actual logic lives inside string formatting and re-parsing, that logic belongs in real code instead.

Advertisement