The Number Miner
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
- L3
- Asked in
- Equifax
Problem
You're auditing a batch of JSON event payloads whose objects and arrays nest arbitrarily deep, and you need a tally of the integers buried inside them. Count how many times each genuine integer value appears anywhere across every payload, returning a map from the integer's string form to its total, where floats and booleans never count, not even a whole-looking 100.0 or a bare true.
JSON strings are hiding numeric secrets. Dig them out.
Example 1
Input:
json_strings = ["{\"user\": {\"age\": 25, \"score\": 100}, \"level\": 3}","{\"user\": {\"age\": 25}, \"level\": 3}"]Output:
{"3":2,"25":2,"100":1}Example 2
Input:
json_strings = ["{\"temp\": 72, \"readings\": [5, 5, 3.5], \"active\": true}"]Output:
{"5":2,"72":1}Worked solution and explanation
What this problem really is
Strip the JSON costume and this is a recursive walk that counts integers, with a type filter that has to be exact on two fronts. The first trap is depth: iterate only each object's top-level values and every integer sitting inside a nested object or array is invisible to you, so your totals come out quietly too low. The second trap is bool: in Python bool subclasses int, so isinstance(True, int) is True. Filter with isinstance(val, int) alone and every true/false in a payload lands in your counts, inflating them in a way that still looks plausible. Floats are the mirror case: 100.0 must not count even though it looks whole.
How to get there
Step 1: Parse each payload
Use json.loads to turn each string into a native Python value: a dict, a list, or a scalar. Native types are exactly what let the filter tell an int from a float from a bool downstream.
Step 2: Recurse into objects and arrays
Walk the parsed value recursively. When you hit a dict, descend into its values; when you hit a list, descend into its items; otherwise you are at a leaf. This is what reaches the integers buried below the top level, which a flat pass over obj.values() would skip entirely.
Step 3: Keep genuine integers, count, and stringify
At each leaf, keep it only if isinstance(val, int) and not isinstance(val, bool): the bool half is not optional, it is what stops True/False from being tallied, and floats fail the int check on their own. Count each survivor, then stringify the keys for the final map.
The solution
import json
def count_json_numbers(json_strings):
counts = {}
def walk(node):
if isinstance(node, dict):
for value in node.values():
walk(value)
elif isinstance(node, list):
for item in node:
walk(item)
elif isinstance(node, int) and not isinstance(node, bool):
counts[node] = counts.get(node, 0) + 1
for payload in json_strings:
walk(json.loads(payload))
return {str(num): count for num, count in counts.items()}The recursion reaches buried integers; the bool guard keeps true/false out of the tally.
- Flat pass (wrong)
- for val in obj.values() only sees the top level. On {"user": {"age": 25, "score": 100}, "level": 3} it counts 3 and stops, silently dropping 25 and 100 because they live one level down.
- Recursive walk (correct)
- Descending into every dict value and list item reaches age, score, and level alike, so 25, 100, and 3 all land in the tally, which is why the expected output counts the nested values.
Common follow-up questions
- How would you also collect the float values, keyed separately? (Tests adjusting the leaf filter and deciding whether to count ints and floats separately.)
- What if JSON parsing failed on some strings? (Tests wrapping json.loads in try/except for graceful error handling.)
- What if the input were a huge stream of JSON lines (JSONL) too big for memory? (Tests line-by-line processing for memory efficiency.)
- How would you avoid a stack overflow on maliciously deep nesting? (Tests converting the recursion to an explicit stack to bound memory on pathological depth.)