The Fine Print
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- mid
- Asked in
- Citi
Problem
Return the transaction records that pass a surveillance analyst's query, in their original order. The query is a predicate tree: each leaf compares one field to a value with >, >=, <, <=, ==, or !=, and and, or, and not nodes nest those leaves to any depth. A leaf is false for any record missing the field it names.
Risk rules nest deep. Decide which transactions clear them.
Example 1
Input:
query = {"and":[{"op":">","field":"amount","value":30},{"or":[{"op":"==","field":"region","value":"EU"},{"op":">","field":"amount","value":100}]}]}, records = [{"txn":"t1","amount":45,"region":"US"},{"txn":"t2","amount":20,"region":"EU"},{"txn":"t3","amount":150,"region":"APAC"},{"txn":"t4","amount":60,"region":"EU"}]Output:
[{"txn":"t3","amount":150,"region":"APAC"},{"txn":"t4","amount":60,"region":"EU"}]Example 2
Input:
query = {"and":[{"op":">=","field":"amount","value":50},{"not":{"op":"==","field":"desk","value":"FX"}}]}, records = [{"desk":"FX","order":"o5","amount":200},{"order":"o9","amount":80},{"desk":"RATES","order":"o3","amount":300},{"desk":"EQ","order":"o4","amount":30}]Output:
[{"order":"o9","amount":80},{"desk":"RATES","order":"o3","amount":300}]Worked solution and explanation
Why this problem exists in real interviews
This is a recursive predicate evaluator wearing a trade-surveillance costume. The skill being probed: can you evaluate a boolean tree of and/or/not over comparison leaves, nested to any depth, against a stream of records? Anyone can filter where amount is greater than 30. The trick is that the query is not a flat list of conditions, it is a tree, and the weak candidate carries the SQL WHERE-clause mental model into Python and writes one loop over a list of comparisons. Feed that loop a node with an or inside a not and it either ignores the branch or blows up trying to read node['field'] on a connective dict. Get this wrong and every record that should clear a nested OR branch silently disappears from the result.
The trap everyone walks into
- Naive flat loop
- all(compare(r, c) for c in query['conditions']) assumes query is a list of leaves joined by AND. It cannot represent or/not at all, and it raises KeyError the moment a connective node appears.
- Recursive walk
- matches(record, node) inspects the node's shape: an and/or/not connective recurses on its children, a leaf does the comparison. Depth is unbounded and the same function handles every level.
Break down the requirements
Step 1: Dispatch on the node's shape
A node is a connective if it carries an 'and', 'or', or 'not' key, otherwise it is a comparison leaf with 'field', 'op', and 'value'. Check the connective keys FIRST, before you ever reach for node['field'], so a connective dict never gets mistaken for a leaf.
Step 2: Let all / any / not carry the boolean logic
and maps to all(...) over the recursive results of its clauses, or maps to any(...), and not inverts the single child. all and any short-circuit for free, so a cheap clause that fails an AND stops the walk early. This is where the recursion pays off: each child is just another call to the same function.
Step 3: Evaluate a leaf, and absorb the missing field
Before comparing, ask whether the field is present. If it is absent, the comparison is false by definition, return False rather than letting Python raise on a missing key. With the field present, dispatch the operator string to the matching comparison. Folding the absent-field rule into the leaf keeps every connective above it oblivious to the special case.
Step 4: Keep the records in input order
Filter with a comprehension over records in their original sequence and return the record dicts themselves, not their indices or copies. Order preservation falls out of iterating the input once; nothing sorts or reshuffles.
The solution
import operator
OPS = {
">": operator.gt,
">=": operator.ge,
"<": operator.lt,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
}
def filter_records(records, query):
def matches(record, node):
if "and" in node:
return all(matches(record, clause) for clause in node["and"])
if "or" in node:
return any(matches(record, clause) for clause in node["or"])
if "not" in node:
return not matches(record, node["not"])
field = node["field"]
if field not in record:
return False
return OPS[node["op"]](record[field], node["value"])
return [record for record in records if matches(record, query)]Common follow-up questions
- How would you support an 'in' operator whose value is a list, or a 'between' leaf with two bounds? (Tests whether the leaf evaluator is extensible: a new op should be a new entry in the dispatch table, not a rewrite of the walk.)
- The same query is applied to millions of records. How would you avoid re-walking the tree's structure for every row? (Tests compiling the query once into a closure or predicate function, so per-record work is just the comparisons, not the dispatch.)
- What should happen when a field is present but its type does not support the operator, say comparing a string with a numeric '>' bound? (Tests reasoning about error semantics: coerce, treat as non-match, or surface a validation error before the batch runs.)