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

Recursive predicate walk with operator dispatch
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