Skip to content

Latest commit

 

History

History
1198 lines (774 loc) · 46.7 KB

File metadata and controls

1198 lines (774 loc) · 46.7 KB

Python String Code Review Exercises

Audience: adult learners who need to review, read, and approve Python code (including code generated by AI agents) — not necessarily write it from scratch.

Format: each exercise gives a spec (what the code is supposed to do), an example, and a code snippet. Answer the question, then click Answer to check yourself and read the explanation.

Assumptions (unless an exercise says otherwise):

  • Arguments are non-None.
  • The code runs without a SyntaxError; you're reviewing for behavioral correctness.
  • Python 3.9+ is assumed. Any newer feature is called out explicitly.

Several exercises specifically test whether Java/other-language habits carry over correctly — Python's string methods don't always work the way you'd guess from another language.

There are 50 exercises across 8 topics. Most snippets are correct; some contain common String-handling mistakes for you to catch.


Section 1: Equality & Identity (== vs is)

Exercise 1

Spec: Return True if a and b hold the same text ("hello").

def same_greeting(name):
    a = "hello"
    b = "".join(["h", "e", "l", "l", "o"])
    return a is b

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — is checks object identity, not value equality; b is built at runtime by "".join(...), so it's a different string object even though the characters match. Should use a == b.
  • C. No — "".join(...) returns a list, not a string, so is always compares different types.
  • D. No — string comparison with is requires both strings to be the same length, or Python raises a TypeError.
Answer

Answer: Bis compares object identity. "".join(...) builds a brand-new string object at runtime, so even though it holds the same characters as "hello", a is b is False. Use == to compare content.

Exercise 2

Spec: Return True if two strings have the same content.

def same_greeting(a, b):
    return a == b

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — == on strings in Python checks object identity, like is, not content.
  • C. No — == requires calling .strip() on both strings first, or it always returns False due to hidden whitespace.
  • D. No — == is case-insensitive by default in Python, so "Hi" and "hi" would incorrectly be treated as equal.
Answer

Answer: A== on strings compares content character-by-character, which is exactly what's needed here. (is, from Exercise 1, is the one that checks identity.)

Exercise 3

Spec: Return True, since both variables are assigned the same short string literal.

a = "cat"
b = "cat"
same = (a is b)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — since a and b are different variables, is always compares references and returns False.
  • C. No — Python strings are never interned automatically; is on strings always requires calling sys.intern() explicitly.
  • D. No — comparing strings with is always raises a SyntaxError in Python 3.
Answer

Answer: A — CPython automatically interns short, identifier-like string literals defined this way, so a and b end up pointing at the same object and is returns True. Important caveat: this is a CPython implementation detail, not a language guarantee — don't write code that relies on is for string equality. Always use == in real code (see Exercises 1–2).

Exercise 4

Spec: Return True if two words match, ignoring case. Example: same_word("Cat", "cAT")True.

def same_word(a, b):
    return a == b

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — == is case-sensitive, so "Cat" == "cAT" is False; both strings should be lower-cased (or case-folded) before comparing.
  • C. No — == on strings only compares length in Python, not actual characters.
  • D. No — == between strings always raises a TypeError unless both are explicitly cast with str().
Answer

Answer: B== does an exact, case-sensitive comparison. The spec asks for a case-insensitive match, so the strings need to be normalized first.

Exercise 5

Spec: Return True if two words match, ignoring case.

def same_word(a, b):
    return a.casefold() == b.casefold()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .casefold() is only available in Python 2, not Python 3, so this fails.
  • C. No — .casefold() behaves identically to .upper(), so mixed-case comparisons are unaffected.
  • D. No — calling .casefold() mutates the original strings in place, so a and b are changed after this call.
Answer

Answer: A.casefold() normalizes case for comparison purposes and is even more aggressive than .lower() for some Unicode text (e.g. German ßss), making it the recommended method for case-insensitive comparisons. It returns a new string; nothing is mutated (strings are immutable — see Section 2).

Exercise 6

Spec: Return True if both strings have the same content, ignoring case, or are both None; must not raise an exception.

def same_word_or_none(a, b):
    if a is None or b is None:
        return a is None and b is None
    return a.lower() == b.lower()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — a is None or b is None never evaluates to True in Python, since None cannot be compared with is.
  • C. No — this raises a TypeError whenever exactly one of a or b is None, because of the mixed boolean expression.
  • D. No — .lower() is still called even when both values are None, so this always raises an AttributeError regardless of the guard clause.
Answer

Answer: Ais None is the correct, idiomatic way to check for None in Python (it always works, since None is a singleton). The guard clause short-circuits before .lower() is ever called on a None value, and returns True only when both are None.


Section 2: Immutability & Reassignment

Exercise 7

Spec: Append " world" to name and return the updated string.

def greet(name):
    name + " world"
    return name

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — name + " world" creates a new string, but the result is discarded since it's never assigned; strings are immutable in Python, so name itself never changes. It should be name = name + " world".
  • C. No — the + operator only works between two variables, not a variable and a literal, so this fails at runtime.
  • D. No — string concatenation in Python requires a .concat() method; + is reserved for numbers.
Answer

Answer: B — Every "modifying" string operation returns a new string rather than changing the original. Since the result of name + " world" is never captured, this line does nothing observable, and the original name is returned unchanged.

Exercise 8

Spec: Append " world" to name and return the updated string.

def greet(name):
    name = name + " world"
    return name

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — reassigning name inside the function also changes the caller's original variable, causing an unwanted side effect.
  • C. No — Python strings can't be concatenated with + and a literal in the same expression.
  • D. No — since strings are immutable, this code raises a TypeError at runtime.
Answer

Answer: A — Capturing the result of + by reassigning name is exactly the fix needed for the bug in Exercise 7. Rebinding a local parameter name never affects the caller's variable — see Exercise 12 for the flip side of that.

Exercise 9

Spec: Return the uppercase version of a string, leaving the original string unchanged.

def shout(s):
    return s.upper()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .upper() modifies s in place, so the caller's original string changes too.
  • C. No — .upper() only uppercases the first character of the string.
  • D. No — this returns None, since Python strings are immutable and can't produce an uppercase copy.
Answer

Answer: A.upper() returns a brand-new uppercase string and never touches the original, satisfying the spec exactly.

Exercise 10

Spec: Remove all spaces from a string.

def remove_spaces(s):
    s.replace(" ", "")
    return s

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .replace(" ", "") returns a new string; since the result isn't assigned back to s, the original, unmodified string is returned. It should be return s.replace(" ", "").
  • C. No — .replace() requires a third count argument in Python 3, or it raises a TypeError.
  • D. No — .replace(" ", "") only removes the first space it finds, not all of them.
Answer

Answer: B — Same immutability trap as Exercise 7: .replace() doesn't mutate s, it returns a new string that needs to be captured or returned directly.

Exercise 11

Spec: Remove all spaces from a string.

def remove_spaces(s):
    return s.replace(" ", "")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .replace() treats its first argument as a regular expression, so a literal space won't match correctly.
  • C. No — .replace(" ", "") only removes the first occurrence, leaving the rest of the spaces.
  • D. No — this returns None, because .replace() is a mutating, in-place method with no return value.
Answer

Answer: Astr.replace(old, new) replaces every literal occurrence of old — unlike re.sub, it never treats its argument as a regex (see Exercise 50). Returning it directly correctly fixes Exercise 10.

Exercise 12

Spec: Remove the first character of the string "in place" so the caller sees the change.

def remove_first_char(s):
    s = s[1:]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — s[1:] actually removes the last character, not the first.
  • C. No — Python passes strings by reference, so reassigning s should update the caller's variable, but a missing return breaks the update.
  • D. No — rebinding the local name s only changes the local variable; the caller's original string is untouched, and since there's no return, the new value is lost entirely, regardless of any "pass by reference" assumption.
Answer

Answer: D — There's no way to mutate a str in place in Python — it's immutable — and reassigning a parameter inside a function never affects the variable the caller passed in. (C describes a genuinely common misconception carried over from other languages: Python passes object references by value, so rebinding the local name is not the same as mutating a shared object.) The function needs to return the new string, and the caller needs to capture it.


Section 3: Indexing & Slicing

Exercise 13

Spec: Return the last 3 characters of a string of length ≥ 3. Example: "hello""llo".

def last_three(s):
    return s[-3:-1]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — slicing in Python excludes the end index; s[-3:-1] stops one character short of the end, returning "ll" instead of "llo". It should be s[-3:].
  • C. No — negative indices count from 1, not 0, so -3 actually refers to the 4th-to-last character.
  • D. No — slicing with two negative indices always raises an IndexError.
Answer

Answer: B — Just like range() and Java's substring(), Python slice end-points are exclusive. s[-3:-1] grabs everything from the 3rd-to-last character up to (but not including) the last, missing the final character.

Exercise 14

Spec: Return the last 3 characters of a string of length ≥ 3.

def last_three(s):
    return s[-3:]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — s[-3:] returns everything except the last 3 characters, not the last 3 themselves.
  • C. No — this raises an IndexError whenever the string's length is exactly 3.
  • D. No — negative slice indices only work on lists, not strings, so this fails to run.
Answer

Answer: A — Omitting the end index means "go to the end of the string," so s[-3:] correctly returns the last 3 characters.

Exercise 15

Spec: Print every character in the string, one per line.

def print_chars(s):
    for i in range(len(s) + 1):
        print(s[i])

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — s[i] is zero-indexed from the end of the string, not the beginning, so characters print in reverse order.
  • C. No — range(len(s) + 1) allows i to equal len(s), which is out of bounds for indexing and raises an IndexError.
  • D. No — string indexing with [] returns the character's Unicode code point instead of the character itself, so print() fails.
Answer

Answer: C — Valid indices for a string of length n run from 0 to n - 1. The loop should use range(len(s)).

Exercise 16

Spec: Print every character in the string, one per line.

def print_chars(s):
    for ch in s:
        print(ch)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — iterating directly over a string with for ch in s only works for lists, not strings, so this raises a TypeError.
  • C. No — this skips the last character; the loop should use range(len(s) - 1) instead.
  • D. No — Python iterates strings backward by default, so this prints the characters in reverse order.
Answer

Answer: A — Strings are directly iterable in Python — for ch in s visits every character exactly once, in order. This is the idiomatic alternative to manual indexing (compare Exercise 15).

Exercise 17

Spec: Return True if the string contains the character 'z'. Example: has_z("zoo")True.

def has_z(s):
    return "z" in s

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — the in operator only checks the first character of the string, not the whole string.
  • C. No — in requires both operands to be the same length, or it raises a ValueError.
  • D. No — in checks object identity like is, so this only works if "z" is the exact same object already inside s.
Answer

Answer: Ain performs a substring/membership search across the whole string, correctly finding 'z' at any position, including the first (unlike a naive index() check using > 0 — see Exercise 21's Java counterpart).

Exercise 18

Spec: Return the extension of a filename (the text after the last .), or an empty string if there's no .. Example: "report.final.pdf""pdf".

def extension(filename):
    dot = filename.rfind(".")
    if dot == -1:
        return ""
    return filename[dot + 1:]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .rfind(".") finds the first dot, not the last, so "report.final.pdf" would incorrectly return "final.pdf".
  • C. No — filename[dot + 1:] raises an IndexError when the filename ends in a dot.
  • D. No — this returns None instead of an empty string when there is no dot in the filename.
Answer

Answer: A.rfind() correctly searches from the right for the last match, and the -1 check correctly handles filenames with no extension. Slicing past the end of a string never raises — it just returns an empty string.


Section 4: Searching & Comparing

Exercise 19

Spec: Return True if text contains word, ignoring case. Example: contains_word("Hello World", "world")True.

def contains_word(text, word):
    return word in text

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — the in operator is case-sensitive, so "world" in "Hello World" is False; both strings should be lower-cased first.
  • C. No — in can only check for single characters, not multi-character substrings.
  • D. No — in requires the substring to be passed as a list of characters, not a plain string.
Answer

Answer: Bin does an exact, case-sensitive substring search, so the case mismatch causes a missed match here.

Exercise 20

Spec: Return True if text contains word, ignoring case.

def contains_word(text, word):
    return word.lower() in text.lower()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .lower() requires a locale argument to work correctly across languages, so this raises a TypeError.
  • C. No — the two .lower() calls cancel each other out, so this behaves identically to case-sensitive matching.
  • D. No — in already ignores case by default in Python 3, so the .lower() calls actively break the match by reversing case.
Answer

Answer: A — Lower-casing both sides before the in check is the standard, correct way to do a case-insensitive substring search.

Exercise 21

Spec: Return True if the URL uses HTTPS. Example: is_secure("https://example.com")True.

def is_secure(url):
    return url == "https://"

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — == only returns True when url is exactly "https://" with nothing after it; real URLs like "https://example.com" incorrectly return False. Should use url.startswith("https://").
  • C. No — == ignores everything after the first colon, so this only checks the protocol name.
  • D. No — this raises a TypeError whenever the URL doesn't start with "http".
Answer

Answer: B — The spec asks whether the URL starts with "https://", not whether it is exactly that string. == requires a full exact match, which real URLs won't satisfy.

Exercise 22

Spec: Return True if the URL uses HTTPS.

def is_secure(url):
    return url.startswith("https://")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .startswith() is case-insensitive, so it would also incorrectly match "HTTPS://" as a different protocol.
  • C. No — .startswith() checks only the first character, not the whole prefix, so "hxxps://" would also match.
  • D. No — .startswith() returns the matching index as an int, not a bool, so this can't be used in a boolean context.
Answer

Answer: A.startswith() correctly checks the beginning of the string against the literal prefix and returns an actual bool. It's case-sensitive, which is what's wanted here.

Exercise 23

Spec: Return True if a comes before b alphabetically.

def comes_before(a, b):
    return a < b

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — < between strings in Python compares their lengths, not alphabetical order.
  • C. No — < is only defined for numbers in Python; using it on strings raises a TypeError.
  • D. No — < is case-insensitive, so "Zebra" < "apple" would compare only by length.
Answer

Answer: A — Python strings support direct comparison operators, doing a lexicographic (dictionary-order) comparison by Unicode code point. < correctly reports whether a sorts before b.

Exercise 24

Spec: Return True if text ends with the word "end".

def ends_with_word(text):
    return text.endswith("end")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .endswith() also matches the substring anywhere inside the string, not just at the end, so "the end is near" would incorrectly return True.
  • C. No — .endswith() is case-insensitive, so "the END" would also match "end".
  • D. No — .endswith() returns the matching index as an int, not a bool, so this can't be used in a boolean context.
Answer

Answer: A.endswith() specifically checks the suffix, unlike .find()/in, which just detect a match anywhere. "the end is near".endswith("end") correctly returns False (it ends in "near"), while "weekend".endswith("end") correctly returns True.


Section 5: Case & Whitespace

Exercise 25

Spec: Return True if the string is empty or contains only whitespace.

def is_blank(s):
    return len(s) == 0

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — len(s) == 0 raises an exception whenever s contains only whitespace.
  • C. No — len(s) == 0 only returns True for a truly empty string; a string like " " is not empty, so this misses whitespace-only input. Should use s.strip() == "" (or not s.strip()).
  • D. No — len() returns -1 for whitespace-only strings in Python, so this comparison always fails anyway, coincidentally for the wrong reason.
Answer

Answer: Clen() only measures character count, not content. A string of pure spaces has len() > 0, so this check misses it entirely.

Exercise 26

Spec: Return True if the string is empty or contains only whitespace.

def is_blank(s):
    return s.strip() == ""

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .strip() only removes leading whitespace, not trailing, so " hi " would still be considered blank.
  • C. No — .strip() returns a new string object, so comparing it with == always checks the wrong object and returns False.
  • D. No — this incorrectly reports "cat" as blank, because .strip() also removes ordinary letters that resemble whitespace.
Answer

Answer: A.strip() removes whitespace from both ends; if nothing remains, the string was blank (or empty) to begin with. == compares content, not identity, so it works correctly here regardless of .strip() creating a new object.

Exercise 27

Spec: Remove the marker substring "xx" from the start and end of a string, if present. Example: strip_xx("xxxylophonexx")"xylophone".

def strip_xx(s):
    return s.strip("xx")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .strip() can only take a single character as its argument, so passing "xx" raises a TypeError.
  • C. No — .strip("xx") treats its argument as a set of characters to remove from each end, not a literal substring; every leading/trailing 'x' character gets stripped, however many there are. strip_xx("xxxylophonexx") actually returns "ylophone" — it strips all three leading x characters (the "xx" marker and the real leading x of "xylophone") — not "xylophone". Use .removeprefix("xx").removesuffix("xx") (Python 3.9+) for literal substring removal.
  • D. No — .strip("xx") removes whitespace as usual and silently ignores the "xx" argument entirely.
Answer

Answer: C — This is one of the most common Python string gotchas: str.strip(chars) (and lstrip/rstrip) treat chars as a set of characters to remove, not a literal prefix/suffix string. Any run of characters from that set at either end gets stripped, no matter how many. It only "looks correct" on inputs where the extra characters aren't adjacent to more of the same character.

Exercise 28

Spec: Remove the marker substring "xx" from the start and end of a string, if present. (Python 3.9+)

def strip_xx(s):
    return s.removeprefix("xx").removesuffix("xx")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .removeprefix() and .removesuffix() both treat their argument as a set of characters too, the same bug as .strip().
  • C. No — chaining .removeprefix(...).removesuffix(...) applies both operations to the start of the string only, never touching the end.
  • D. No — .removeprefix("xx") raises a ValueError if the string doesn't start with "xx", so this crashes on ordinary input.
Answer

Answer: A — Unlike .strip(), .removeprefix()/.removesuffix() (added in Python 3.9) remove a literal substring exactly once, and do nothing (no error) if the string doesn't start/end with it. This correctly fixes Exercise 27's bug.

Exercise 29

Spec: Capitalize the first letter of each word and lowercase the rest. Words are separated by single spaces. Example: "hello WORLD""Hello World".

def title_case(s):
    return s.capitalize()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .capitalize() capitalizes every letter, not just the first one, effectively acting like .upper().
  • C. No — .capitalize() raises a ValueError on strings containing more than one word.
  • D. No — .capitalize() only capitalizes the first letter of the entire string, lowercasing everything else; "hello WORLD".capitalize() returns "Hello world", not "Hello World". Use .title(), or capitalize each word individually.
Answer

Answer: D.capitalize() operates on the whole string as one unit — only its very first character is uppercased, and the rest is forced to lowercase, including the first letters of later words.

Exercise 30

Spec: Capitalize the first letter of each word and lowercase the rest.

def title_case(s):
    return s.title()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .title() only capitalizes the first word, behaving identically to .capitalize().
  • C. No — .title() requires an explicit list of word-boundary characters to be passed as an argument, or it raises a TypeError.
  • D. No — .title() removes the spaces between words, collapsing "Hello World" into "HelloWorld".
Answer

Answer: A.title() capitalizes the first letter of every word and lowercases the rest of each word, which is exactly what's asked for here. (For text with apostrophes or embedded caps like "o'brien", .title() has known quirks — but that's outside this spec.)


Section 6: Splitting & Joining

Exercise 31

Spec: Split a version string like "1.2.3" into its three numeric parts.

def parts(version):
    return version.split(".")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .split() treats its argument as a regular expression, and an unescaped . matches any character, splitting the string apart everywhere.
  • C. No — .split(".") only works for strings with exactly one dot; more than one dot raises a ValueError.
  • D. No — .split() requires escaping special regex characters like . with a backslash, or it silently returns an empty list.
Answer

Answer: A — Unlike String.split() in Java (or re.split()/re.sub() in Python — see Exercise 50), Python's str.split(sep) always treats sep as a literal string, never a regular expression. "1.2.3".split(".") correctly returns ["1", "2", "3"] with no escaping needed. Assuming otherwise is a common Java→Python transfer mistake.

Exercise 32

Spec: Split a sentence into words, correctly handling multiple consecutive spaces (no empty strings in the result). Example: "hello world"["hello", "world"].

def words(sentence):
    return sentence.split(" ")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .split(" ") raises a ValueError whenever there is more than one space between words.
  • C. No — passing an explicit " " separator does not collapse consecutive delimiters; "hello world".split(" ") returns ["hello", "", "", "world"], with an empty string for each extra space. Calling .split() with no arguments splits on any run of whitespace and discards empty results.
  • D. No — .split(" ") only returns the first two words and silently drops the rest.
Answer

Answer: C — An explicit separator splits at every single occurrence, producing an empty string between adjacent delimiters. .split() with no argument is special-cased to split on runs of whitespace and never produces empty strings.

Exercise 33

Spec: Split a sentence into words, correctly handling multiple consecutive spaces.

def words(sentence):
    return sentence.split()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — calling .split() with no arguments splits the string into individual characters instead of words.
  • C. No — .split() with no arguments only works on strings that start with whitespace.
  • D. No — .split() with no arguments raises a TypeError unless a separator is explicitly provided.
Answer

Answer: A.split() with no arguments splits on any run of whitespace (spaces, tabs, newlines) and ignores leading/trailing whitespace entirely, correctly fixing Exercise 32's bug.

Exercise 34

Spec: Join a list of numbers into a comma-separated string. Example: [1, 2, 3]"1,2,3".

def join_numbers(numbers):
    return ",".join(numbers)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .join() can only join exactly two elements at a time; lists longer than 2 raise a ValueError.
  • C. No — ",".join(numbers) inserts the comma before the first element too, producing ",1,2,3".
  • D. No — .join() requires every element of the iterable to already be a str; passing a list of ints raises a TypeError. Each number needs converting first, e.g. ",".join(str(n) for n in numbers).
Answer

Answer: Dstr.join() is strict about types: every item in the iterable must already be a string. ",".join([1, 2, 3]) raises TypeError: sequence item 0: expected str instance, int found.

Exercise 35

Spec: Join a list of numbers into a comma-separated string.

def join_numbers(numbers):
    return ",".join(str(n) for n in numbers)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — using a generator expression inside .join() only processes the first element and ignores the rest.
  • C. No — str(n) converts each number to a string representation with extra quote characters included, like '"1"'.
  • D. No — .join() cannot accept a generator expression, only a list or tuple, so this fails at runtime.
Answer

Answer: A.join() happily accepts any iterable of strings, including a generator expression, and consumes it fully. Converting each number with str(n) first correctly fixes Exercise 34's TypeError.

Exercise 36

Spec: Join a list of words with commas, with no trailing comma. Example: ["a", "b", "c"]"a,b,c".

def join_words(words):
    result = ""
    for i, w in enumerate(words):
        result += w
        if i < len(words) - 1:
            result += ","
    return result

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — enumerate() starts counting from 1, not 0, so the condition i < len(words) - 1 skips the second-to-last word instead of the last.
  • C. No — the condition i < len(words) - 1 skips appending the last word entirely, not just its trailing comma.
  • D. No — result += w raises a TypeError because Python strings don't support the += operator.
Answer

Answer: Aenumerate() starts at 0 by default. The comma is only appended when there's a next element (i < len(words) - 1), so every word gets appended and only the interior gaps get commas. (",".join(words), from Exercise 34/35, is the simpler way to do this, but this manual version is also correct.)


Section 7: Building & Formatting

Exercise 37

Spec: Reverse a string. Example: "hello""olleh".

def reverse(s):
    return s.reverse()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .reverse() reverses the words in the string, not the individual characters.
  • C. No — str has no .reverse() method in Python; this raises an AttributeError. Use slicing instead: s[::-1].
  • D. No — .reverse() works, but only for strings with an even number of characters.
Answer

Answer: Clist has a .reverse() method (which mutates the list in place and returns None), but str does not have one at all — this is a common mistake for anyone used to a language with a built-in string-reverse method. The idiomatic Python fix is slicing: s[::-1].

Exercise 38

Spec: Reverse a string.

def reverse(s):
    return s[::-1]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — s[::-1] returns every other character in reverse, not the full reversed string.
  • C. No — the step value -1 in a slice is only valid for lists, not strings, so this raises a TypeError.
  • D. No — this reverses the words in a sentence, not the individual characters.
Answer

Answer: A — A slice of [start:stop:step] with step = -1 and both start/stop omitted walks the entire string backward, one character at a time — this correctly reverses s and fixes Exercise 37's AttributeError.

Exercise 39

Spec: Return True if the string is a palindrome (reads the same forwards and backwards), case-sensitive. Example: "racecar"True, "hello"False.

def is_palindrome(s):
    return s == s[::-1]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — s[::-1] creates a new string object, so == always compares references and returns False, the same way is would.
  • C. No — palindrome checks must ignore case and punctuation by definition, so this implementation is too strict for any valid input.
  • D. No — slicing with [::-1] only reverses strings with an odd length; even-length strings are left unchanged.
Answer

Answer: A== always compares content in Python, regardless of whether the two strings are the same object, so this correctly compares s to its reverse. (C is a trap: the spec here is explicitly case-sensitive with no punctuation handling — don't assume requirements beyond what's stated; see Exercise 40 for the case-insensitive version.)

Exercise 40

Spec: Return True if the string is a palindrome, ignoring case. Example: "Racecar"True.

def is_palindrome(s):
    lowered = s.lower()
    return lowered == lowered[::-1]

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .lower() must also be called separately on lowered[::-1], or the comparison fails because the reversed copy retains the original case.
  • C. No — this only works correctly for palindromes with an odd number of characters.
  • D. No — .lower() raises a ValueError when the string contains digits or punctuation.
Answer

Answer: Alowered is computed once, up front, and the reversed copy is built from lowered, so it's already all lowercase — a second .lower() call would be redundant, not required.

Exercise 41

Spec: Format a name and age into "Name is Age years old.". Example: describe("Ana", 30)"Ana is 30 years old.".

def describe(name, age):
    return "%d is %s years old." % (name, age)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — the % operator only works with a single value, not a tuple of multiple values.
  • C. No — Python's % formatting ignores mismatched types and just prints the values in order regardless of the specifier used.
  • D. No — %-style specifiers are matched positionally; %d is matched to name (a str), which raises a TypeError at runtime. It should be "%s is %d years old." % (name, age).
Answer

Answer: D%d expects a number and is matched to the first value in the tuple, name. Since name is a string, this raises TypeError: %d format: a real number is required, not str.

Exercise 42

Spec: Format a name and age into "Name is Age years old.".

def describe(name, age):
    return f"{name} is {age} years old."

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — f-strings evaluate their expressions once, at module import time, so name and age are frozen to whatever values they had the first time the module loaded.
  • C. No — curly braces in f-strings must contain a format specifier like :d or :s, or Python raises a SyntaxError.
  • D. No — f-strings only work with global variables, not function parameters, so this fails to compile.
Answer

Answer: A — f-strings evaluate their {...} expressions fresh, every time the f-string is evaluated (here, every time describe() runs), using whatever name and age are bound to at that moment. No format specifier is required — plain {name} just calls str() on the value.


Section 8: Padding, Numeric Checks & Misc

Exercise 43

Spec: Format an integer with leading zeros to a width of 5. Example: padded(42)"00042".

def padded(n):
    return str(n).rjust(5)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .rjust(5) truncates the number to 5 characters instead of padding it.
  • C. No — .rjust(5) pads with spaces by default, not zeros; padded(42) returns " 42", not "00042". Use .zfill(5), or .rjust(5, "0").
  • D. No — .rjust() only works on lists, not strings, so this fails to run.
Answer

Answer: C.rjust(width) right-justifies with a space fill character by default. To zero-pad, either pass "0" as the second argument to .rjust(), or use the purpose-built .zfill().

Exercise 44

Spec: Format an integer with leading zeros to a width of 5.

def padded(n):
    return str(n).zfill(5)

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .zfill(5) pads with zeros only for negative numbers; positive numbers are still padded with spaces.
  • C. No — this raises a ValueError whenever n is already 5 digits or longer.
  • D. No — .zfill() inserts the zeros in the middle of the number rather than at the front.
Answer

Answer: A.zfill(width) left-pads with "0" up to the given width for any string (including one built from a negative number, where it correctly pads after the minus sign), and simply returns the original string unchanged if it's already at least that long.

Exercise 45

Spec: Return a string of n dashes. Example: repeat_dash(4)"----".

def repeat_dash(n):
    return "-" * n

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — the * operator between a string and an integer raises a TypeError in Python; strings can only be multiplied by other strings.
  • C. No — "-" * n concatenates the string with itself only once, regardless of n, always returning "--".
  • D. No — "-" * n repeats the string n + 1 times due to an off-by-one in how Python implements *.
Answer

Answer: A — Python's * operator between a str and an int is defined as repetition: "-" * 4 correctly returns "----".

Exercise 46

Spec: Return the uppercase text of a value, or the text "NONE" if the value is None.

def loud(value):
    return value.upper()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — Python automatically converts None to the string "NONE" before any method call, so this line never executes for None input.
  • C. No — .upper() returns None when given an all-lowercase string, so loud("hi") would incorrectly return None.
  • D. No — if value is None, .upper() raises an AttributeError since None has no upper method; the function needs an explicit None check before calling .upper().
Answer

Answer: DNone doesn't have string methods, so None.upper() raises AttributeError: 'NoneType' object has no attribute 'upper'. There's no automatic conversion — the None case has to be handled explicitly (see Exercise 47).

Exercise 47

Spec: Return the uppercase text of a value, or the text "NONE" if the value is None.

def loud(value):
    if value is None:
        return "NONE"
    return value.upper()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — comparing with is None instead of == None means the check never actually matches when value is None.
  • C. No — this raises a NameError because value is checked before it's guaranteed to exist.
  • D. No — the if block executes even when value is a non-empty string, since Python treats all objects as None-like by default in conditionals.
Answer

Answer: Ais None is the correct, idiomatic None check (PEP 8 recommends it over == None). It correctly short-circuits before .upper() is ever called on None, fixing Exercise 46's AttributeError.

Exercise 48

Spec: Return True if the string represents a valid integer that int() can parse, including an optional leading - sign. Example: is_valid_int("-42")True.

def is_valid_int(s):
    return s.isdigit()

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .isdigit() raises a ValueError on any string longer than 2 characters.
  • C. No — .isdigit() returns False for any string containing a - or + sign, since those aren't digit characters; is_valid_int("-42") incorrectly returns False even though int("-42") works fine.
  • D. No — .isdigit() returns True for decimal numbers like "4.2", so this incorrectly accepts non-integer input.
Answer

Answer: C.isdigit() checks whether every character is a digit character. A leading sign character fails that check immediately, even though int() handles signed integers just fine. .isdigit() also isn't a reliable stand-in for "is this parseable by int()" in general — the direct try/except approach in Exercise 49 is more robust.

Exercise 49

Spec: Return True if the string represents a valid integer that int() can parse.

def is_valid_int(s):
    try:
        int(s)
        return True
    except ValueError:
        return False

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — try/except blocks in Python can only catch TypeError, not ValueError, so this fails to catch invalid input.
  • C. No — calling int(s) inside the try block permanently converts s to an integer everywhere else in the program too.
  • D. No — this incorrectly returns True for float strings like "4.2", since int() silently truncates them instead of raising.
Answer

Answer: Aint(s) raises ValueError for anything it can't parse as an integer — including "4.2" (it does not silently truncate floats-as-strings) — so this correctly reports validity for exactly the strings int() can handle, signed or not.

Exercise 50

Spec: Remove all periods from a string. Example: "a.b.c""abc".

def remove_dots(s):
    return s.replace(".", "")

Does this code do this correctly? If not, why not?

  • A. Yes, it is correct.
  • B. No — .replace() treats its first argument as a regular expression, so a literal "." won't correctly match every period.
  • C. No — .replace(".", "") only removes the first occurrence of ".", leaving the rest.
  • D. No — this returns None, because .replace() mutates the string in place and has no return value.
Answer

Answer: Astr.replace() always treats its arguments as literal substrings, never a regex, and replaces every occurrence by default. Watch out for the opposite trap with re.sub(): re.sub(".", "", s) would treat "." as "match any character" and wipe out the entire string, returning "" instead of "abc" — a real regex object is needed there (re.sub(r"\.", "", s)) to get literal-dot behavior.


Answer Key (quick reference)

# Ans # Ans # Ans # Ans
1 B 14 A 27 C 40 A
2 A 15 C 28 A 41 D
3 A 16 A 29 D 42 A
4 B 17 A 30 A 43 C
5 A 18 A 31 A 44 A
6 A 19 B 32 C 45 A
7 B 20 A 33 A 46 D
8 A 21 B 34 D 47 A
9 A 22 A 35 A 48 C
10 B 23 A 36 A 49 A
11 A 24 A 37 C 50 A
12 D 25 C 38 A
13 B 26 A 39 A