6. Split a Multi-Question Email into Distinct Questions
Problem
Fin's email channel often receives messages where a customer bundles several unrelated asks into a single email: a greeting, a bit of context, then two or three distinct questions dropped in as a numbered or bulleted list (or just run together as separate sentences). Before Fin can produce a useful reply, it needs to break the raw email body apart into the individual questions so each one gets answered directly instead of being blended into one vague response.
Write a function that takes the raw email body and returns a list of the individual questions found in it, in order.
Source: src/6_email_multi_question_splitter.py
def split_questions(email_body: str) -> list[str]
>>> split_questions(
... "Hi, I have two questions.\n"
... "1. How do I reset my password?\n"
... "2. Can I get a refund for order #123?"
... )
['How do I reset my password?', 'Can I get a refund for order #123?']
>>> split_questions("Can I get a refund for my order?")
['Can I get a refund for my order?']
Step-by-Step Approach
- Split the email body into lines — numbered/bulleted lists are the strongest, most explicit signal a customer is asking multiple questions.
- For each non-blank line, check whether it starts with a list marker: a digit followed by
.or), or a leading-, then whitespace. - If a line has a marker, strip the marker off and treat the remainder as a question outright — the customer already did the segmentation work for you, so don't require a trailing
?. - If a line has no marker, it may still contain one or more questions run together as prose (e.g. "Hi, can you help? Also, what's your refund policy?"). Split that line into sentences on sentence-ending punctuation.
- Keep only the sentences that end in
?— this filters out greetings, context, and other non-question prose that isn't part of a list. - Append everything you keep, in the order encountered, to the result list.
The key insight is that a numbered/bulleted line is unconditional evidence of a distinct question (no need to re-check for a question mark), while free-form prose needs a stricter filter — split into sentences and require punctuation — to avoid pulling in unrelated statements.
Reference solution
import re
_MARKER_RE = re.compile(r"^(?:\d+[.)]|-)\s+")
_SENTENCE_SPLIT_RE = re.compile(r"(?<=[.?!])\s+")
def split_questions(email_body: str) -> list[str]:
# Line scan: numbered/bulleted lines are questions outright; other lines are split
# into sentences and only the ones ending in '?' are kept. O(n) in body length.
questions: list[str] = []
# split on "\n", not splitlines(), for simple indices
for raw_line in email_body.split("\n"):
line = raw_line.strip()
if not line:
continue
# match() anchors at start, unlike search()
marker = _MARKER_RE.match(line)
if marker:
# end() gives index just past the marker
content = line[marker.end():].strip()
if content:
questions.append(content)
continue
# lookbehind keeps punctuation attached
for sentence in _SENTENCE_SPLIT_RE.split(line):
sentence = sentence.strip()
if sentence.endswith("?"):
questions.append(sentence)
return questions
Key Functions & Tricks
re.compile(pattern)— precompile once at module load instead of per-callr"^(?:\d+[.)]|-)\s+"— non-capturing group matches "1." "2)" or "-" list markerspattern.match(line)— anchors at start only; returnsMatchorNonematch.end()— index just past the match, for slicing off the markerr"(?<=[.?!])\s+"— lookbehind split keeps sentence-ending punctuation attachedpattern.split(line)— splits on every match, O(n) in line lengthstr.split("\n")— used oversplitlines()for simple, predictable line indices
How to Recognize This Pattern
Reach for this line-scan-plus-sentence-split approach whenever a problem asks you to pull structured items (questions, action items, requirements) out of unstructured free text that sometimes uses explicit formatting (numbering, bullets) and sometimes doesn't. The general recipe is: look for the strong, unambiguous signal first (explicit markers) and fall back to a weaker heuristic (punctuation, keyword, regex) only when the strong signal is absent. A common variation is extracting bullet/numbered action items regardless of trailing punctuation, or detecting embedded URLs/quotes the same way. A common pitfall is applying the same strict rule (e.g. "must end in ?") to both the marked and unmarked cases — that silently drops content the customer explicitly separated for you, even when it isn't phrased as a question.