11. How would you implement wildcard and null-aware lookup logic in Python?
Explain Python representation choices for records whose lookup dimensions may be exact values, wildcards, or nulls. Cover matching order, trie-like indexing, ambiguity, missing fields, and performance tradeoffs.
I would use None only for an explicit null value and use private sentinel objects for a wildcard and a missing query field. I would store rules in a nested dictionary with one level per lookup dimension. During lookup, I would explore the exact branch and the wildcard branch, count exact matches, and return the unique rule with the highest specificity. A missing query field can match only a wildcard. If several different rules have the same best specificity, I would raise an ambiguity error.
See the Code while reading this explanation.
I would represent exact values, nulls, wildcards, and missing fields as separate states. None means an explicit null. A private ANY sentinel means any value. A separate MISSING sentinel means the query did not provide that field. This matters because query.get alone cannot distinguish a missing key from a key whose value is None.
- Should I focus on Python language behavior, or also explain the runtime and standard library?
- Which Python version and execution environment should I assume?
- Would you like a small code example together with production tradeoffs and edge cases?
I would index rules in a nested dictionary with one level for each lookup dimension. A rule that omits a dimension uses ANY for that level. During lookup, an exact branch, including a None branch, adds one specificity point. An ANY branch adds no point. A missing query field can follow only ANY because there is no exact value to compare.
The lookup returns the unique rule with the highest specificity. If different rules share the best score, it raises an ambiguity error instead of depending on insertion order. Building the index costs O(n times d) time and up to O(n times d) memory, where n is the number of rules and d is the number of dimensions. Prefix sharing can reduce memory. Lookup may explore up to O(2 to the power d) paths in the worst case, so this design works best with a small, fixed number of dimensions.
The code uses ANY as the wildcard sentinel, MISSING as the absent query field sentinel, and LEAF as a private storage key for rules. None remains an ordinary exact lookup value. Rules are inserted into a nested dictionary in a fixed dimension order. An omitted rule dimension becomes ANY. Lookup explores an exact branch when the query contains the field and also explores an ANY branch when one exists. Each exact branch adds one specificity point. Missing query fields can follow only ANY. The code returns the unique rule with the highest score and raises AmbiguousMatchError when different rules share that score. Index construction uses O(n times d) time and up to O(n times d) memory. Worst case lookup explores O(2 to the power d) paths.
from dataclasses import dataclass
from typing import Any as TypingAny
# Private sentinel for a wildcard rule value.
ANY = object()
# Private sentinel for a query field that is not present.
MISSING = object()
# Private sentinel used to store rules at a leaf node.
LEAF = object()
class AmbiguousMatchError(Exception):
"""Raised when different rules have the same best specificity."""
@dataclass(frozen=True)
class Rule:
name: str
values: dict[str, TypingAny]
result: str
class RuleIndex:
def __init__(self, dimensions: tuple[str, ...]) -> None:
# The dimension order must be stable for insertion and lookup.
self.dimensions = dimensions
self.root: dict[TypingAny, TypingAny] = {}
def add(self, rule: Rule) -> None:
"""Insert one rule into the nested dictionary index."""
node = self.root
for dimension in self.dimensions:
# An omitted rule dimension means wildcard behavior.
key = rule.values.get(dimension, ANY)
node = node.setdefault(key, {})
# More than one rule may occupy the same leaf.
node.setdefault(LEAF, []).append(rule)
def lookup(self, query: dict[str, TypingAny]) -> Rule | None:
"""Return the unique most specific rule for the query."""
matches: list[tuple[int, Rule]] = []
def visit(
node: dict[TypingAny, TypingAny],
depth: int,
specificity: int,
) -> None:
# All dimensions have been processed.
if depth == len(self.dimensions):
for rule in node.get(LEAF, []):
matches.append((specificity, rule))
return
dimension = self.dimensions[depth]
query_value = query.get(dimension, MISSING)
# A present query field may follow its exact branch.
# None is treated as an exact value here.
if query_value is not MISSING and query_value in node:
visit(
node[query_value],
depth + 1,
specificity + 1,
)
# A wildcard can match any value and can also match a missing field.
if ANY in node:
visit(
node[ANY],
depth + 1,
specificity,
)
visit(self.root, 0, 0)
if not matches:
return None
best_score = max(score for score, _ in matches)
best_rules = [rule for score, rule in matches if score == best_score]
# Remove repeated references to the same Rule object only.
unique_rules: list[Rule] = []
seen_ids: set[int] = set()
for rule in best_rules:
rule_id = id(rule)
if rule_id not in seen_ids:
seen_ids.add(rule_id)
unique_rules.append(rule)
if len(unique_rules) > 1:
names = ", ".join(sorted(rule.name for rule in unique_rules))
raise AmbiguousMatchError(f"Ambiguous rules at specificity {best_score}: {names}")
return unique_rules[0]
if __name__ == "__main__":
index = RuleIndex(("country", "device", "tier"))
index.add(
Rule(
name="us mobile null tier",
values={
"country": "US",
"device": "mobile",
"tier": None,
},
result="rule A",
)
)
index.add(
Rule(
name="us wildcard device and tier",
values={"country": "US"},
result="rule B",
)
)
index.add(
Rule(
name="global default",
values={},
result="rule C",
)
)
first = index.lookup({"country": "US", "device": "mobile", "tier": None})
second = index.lookup({"country": "US", "device": "desktop"})
third = index.lookup({"country": "CA"})
print(first.result if first else None)
print(second.result if second else None)
print(third.result if third else None)This design is useful for configuration selection, feature rules, pricing rules, routing policies, access rules, and content selection. For example, one rule can match country US, device mobile, and an explicit null customer tier. Another rule can match country US with any device and any tier. A final wildcard rule can act as the global default.
Interviewers ask this question to test whether a candidate can represent exact values, explicit null values, wildcards, and missing fields without confusing them. It also evaluates Python data structure knowledge, deterministic matching rules, ambiguity handling, and judgment about lookup speed and memory use.
Common mistakes include using None for both null and wildcard, using query.get without a missing sentinel, treating a missing field as an explicit null, returning the first match found, and using insertion order to hide ambiguous rules. Another mistake is claiming lookup always costs O(d). Exact and wildcard branches may both exist at each level, so worst case lookup can explore O(2 to the power d) paths. It is also a mistake to ignore the memory used by nested dictionary nodes.
Start with the representation rule. Say that None means explicit null, ANY means wildcard, and MISSING means an absent query field. Then explain specificity scoring, ambiguity handling, and the worst case branching and memory costs.









