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.
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 bDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No —
ischecks object identity, not value equality;bis built at runtime by"".join(...), so it's a different string object even though the characters match. Should usea == b. - C. No —
"".join(...)returns a list, not a string, soisalways compares different types. - D. No — string comparison with
isrequires both strings to be the same length, or Python raises aTypeError.
Answer
Answer: B — is 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.
Spec: Return True if two strings have the same content.
def same_greeting(a, b):
return a == bDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No —
==on strings in Python checks object identity, likeis, not content. - C. No —
==requires calling.strip()on both strings first, or it always returnsFalsedue 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.)
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
aandbare different variables,isalways compares references and returnsFalse. - C. No — Python strings are never interned automatically;
ison strings always requires callingsys.intern()explicitly. - D. No — comparing strings with
isalways raises aSyntaxErrorin 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).
Spec: Return True if two words match, ignoring case. Example: same_word("Cat", "cAT") → True.
def same_word(a, b):
return a == bDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No —
==is case-sensitive, so"Cat" == "cAT"isFalse; 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 aTypeErrorunless both are explicitly cast withstr().
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.
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, soaandbare 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).
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 Nonenever evaluates toTruein Python, sinceNonecannot be compared withis. - C. No — this raises a
TypeErrorwhenever exactly one ofaorbisNone, because of the mixed boolean expression. - D. No —
.lower()is still called even when both values areNone, so this always raises anAttributeErrorregardless of the guard clause.
Answer
Answer: A — is 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.
Spec: Append " world" to name and return the updated string.
def greet(name):
name + " world"
return nameDoes 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, sonameitself never changes. It should bename = 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.
Spec: Append " world" to name and return the updated string.
def greet(name):
name = name + " world"
return nameDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No — reassigning
nameinside 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
TypeErrorat 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.
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()modifiessin 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.
Spec: Remove all spaces from a string.
def remove_spaces(s):
s.replace(" ", "")
return sDoes 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 tos, the original, unmodified string is returned. It should bereturn s.replace(" ", ""). - C. No —
.replace()requires a thirdcountargument in Python 3, or it raises aTypeError. - 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.
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: A — str.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.
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
sshould update the caller's variable, but a missingreturnbreaks the update. - D. No — rebinding the local name
sonly changes the local variable; the caller's original string is untouched, and since there's noreturn, 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.
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 bes[-3:]. - C. No — negative indices count from
1, not0, so-3actually 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.
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
IndexErrorwhenever 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.
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)allowsito equallen(s), which is out of bounds for indexing and raises anIndexError. - D. No — string indexing with
[]returns the character's Unicode code point instead of the character itself, soprint()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)).
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 sonly works for lists, not strings, so this raises aTypeError. - 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).
Spec: Return True if the string contains the character 'z'. Example: has_z("zoo") → True.
def has_z(s):
return "z" in sDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No — the
inoperator only checks the first character of the string, not the whole string. - C. No —
inrequires both operands to be the same length, or it raises aValueError. - D. No —
inchecks object identity likeis, so this only works if"z"is the exact same object already insides.
Answer
Answer: A — in 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).
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 anIndexErrorwhen the filename ends in a dot. - D. No — this returns
Noneinstead 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.
Spec: Return True if text contains word, ignoring case. Example: contains_word("Hello World", "world") → True.
def contains_word(text, word):
return word in textDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No — the
inoperator is case-sensitive, so"world" in "Hello World"isFalse; both strings should be lower-cased first. - C. No —
incan only check for single characters, not multi-character substrings. - D. No —
inrequires the substring to be passed as a list of characters, not a plain string.
Answer
Answer: B — in does an exact, case-sensitive substring search, so the case mismatch causes a missed match here.
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 alocaleargument to work correctly across languages, so this raises aTypeError. - C. No — the two
.lower()calls cancel each other out, so this behaves identically to case-sensitive matching. - D. No —
inalready 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.
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 returnsTruewhenurlis exactly"https://"with nothing after it; real URLs like"https://example.com"incorrectly returnFalse. Should useurl.startswith("https://"). - C. No —
==ignores everything after the first colon, so this only checks the protocol name. - D. No — this raises a
TypeErrorwhenever 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.
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 anint, not abool, 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.
Spec: Return True if a comes before b alphabetically.
def comes_before(a, b):
return a < bDoes 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 aTypeError. - 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.
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 returnTrue. - C. No —
.endswith()is case-insensitive, so"the END"would also match"end". - D. No —
.endswith()returns the matching index as anint, not abool, 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.
Spec: Return True if the string is empty or contains only whitespace.
def is_blank(s):
return len(s) == 0Does this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No —
len(s) == 0raises an exception wheneverscontains only whitespace. - C. No —
len(s) == 0only returnsTruefor a truly empty string; a string like" "is not empty, so this misses whitespace-only input. Should uses.strip() == ""(ornot s.strip()). - D. No —
len()returns-1for whitespace-only strings in Python, so this comparison always fails anyway, coincidentally for the wrong reason.
Answer
Answer: C — len() only measures character count, not content. A string of pure spaces has len() > 0, so this check misses it entirely.
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 returnsFalse. - 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.
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 aTypeError. - 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 leadingxcharacters (the"xx"marker and the real leadingxof "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.
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 aValueErrorif 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.
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 aValueErroron 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.
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 aTypeError. - 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.)
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 aValueError. - 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.
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 aValueErrorwhenever 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.
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 aTypeErrorunless 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.
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 aValueError. - 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 astr; passing a list ofints raises aTypeError. Each number needs converting first, e.g.",".join(str(n) for n in numbers).
Answer
Answer: D — str.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.
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.
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 resultDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No —
enumerate()starts counting from1, not0, so the conditioni < len(words) - 1skips the second-to-last word instead of the last. - C. No — the condition
i < len(words) - 1skips appending the last word entirely, not just its trailing comma. - D. No —
result += wraises aTypeErrorbecause Python strings don't support the+=operator.
Answer
Answer: A — enumerate() 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.)
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 —
strhas no.reverse()method in Python; this raises anAttributeError. Use slicing instead:s[::-1]. - D. No —
.reverse()works, but only for strings with an even number of characters.
Answer
Answer: C — list 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].
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
-1in a slice is only valid for lists, not strings, so this raises aTypeError. - 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.
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 returnsFalse, the same wayiswould. - 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.)
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 onlowered[::-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 aValueErrorwhen the string contains digits or punctuation.
Answer
Answer: A — lowered 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.
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;%dis matched toname(astr), which raises aTypeErrorat 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.
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
nameandageare 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
:dor:s, or Python raises aSyntaxError. - 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.
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().
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
ValueErrorwhenevernis 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.
Spec: Return a string of n dashes. Example: repeat_dash(4) → "----".
def repeat_dash(n):
return "-" * nDoes 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 aTypeErrorin Python; strings can only be multiplied by other strings. - C. No —
"-" * nconcatenates the string with itself only once, regardless ofn, always returning"--". - D. No —
"-" * nrepeats the stringn + 1times 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 "----".
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
Noneto the string"NONE"before any method call, so this line never executes forNoneinput. - C. No —
.upper()returnsNonewhen given an all-lowercase string, soloud("hi")would incorrectly returnNone. - D. No — if
valueisNone,.upper()raises anAttributeErrorsinceNonehas nouppermethod; the function needs an explicitNonecheck before calling.upper().
Answer
Answer: D — None 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).
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 Noneinstead of== Nonemeans the check never actually matches whenvalueisNone. - C. No — this raises a
NameErrorbecausevalueis checked before it's guaranteed to exist. - D. No — the
ifblock executes even whenvalueis a non-empty string, since Python treats all objects asNone-like by default in conditionals.
Answer
Answer: A — is 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.
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 aValueErroron any string longer than 2 characters. - C. No —
.isdigit()returnsFalsefor any string containing a-or+sign, since those aren't digit characters;is_valid_int("-42")incorrectly returnsFalseeven thoughint("-42")works fine. - D. No —
.isdigit()returnsTruefor 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.
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 FalseDoes this code do this correctly? If not, why not?
- A. Yes, it is correct.
- B. No —
try/exceptblocks in Python can only catchTypeError, notValueError, so this fails to catch invalid input. - C. No — calling
int(s)inside thetryblock permanently convertssto an integer everywhere else in the program too. - D. No — this incorrectly returns
Truefor float strings like"4.2", sinceint()silently truncates them instead of raising.
Answer
Answer: A — int(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.
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: A — str.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.
| # | 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 |