Moving Day

A hard Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.

Domain
Python
Difficulty
hard
Seniority
L5
Asked in
Google

Problem

A schema migration is an ordered list of operations run against every stored record: rename a field, add one with a default, remove one, or cast a field to int, float, or str. A field's path is dot-separated so an operation can reach into a nested record like addr.country, and add builds any intermediate records that are missing while rename, remove, and cast skip a path that isn't there. Casting is lenient: a value that will not convert, such as None, is left as it is.

Old schema in, new schema out.

Example 1

Input:

records = [{"age":"30","tmp":"x","addr":{"zip":"10001"},"name":"Alice"}], operations = [{"op":"rename","path":"name","new_name":"full_name"},{"op":"cast","path":"age","target_type":"int"},{"op":"remove","path":"tmp"},{"op":"add","path":"addr.country","default":"US"}]

Output:

[{"age":30,"addr":{"zip":"10001","country":"US"},"full_name":"Alice"}]

Example 2

Input:

records = [{"price":"19.99","legacy":"drop-me"}], operations = [{"op":"cast","path":"price","target_type":"int"},{"op":"remove","path":"legacy"},{"op":"add","path":"meta.version","default":2},{"op":"cast","path":"missing","target_type":"float"}]

Output:

[{"meta":{"version":2},"price":19}]

Worked solution and explanation

What this really is

Strip off the migration costume and this is a tiny interpreter: each operation names a place in the record (a dot-path) and an action (rename, add, remove, cast). The real skill is separating the two, walk the path to the parent container and the final key, then dispatch on the op kind. Anyone can rewrite a flat field. The trap is that 'addr.country' is not a key, it is a route, and only 'add' is allowed to build that route as it walks it. Miss that and 'add' throws a KeyError on the first missing parent; copy the record shallowly and your nested writes quietly poison the caller's input.


Break down the requirements

Step 1: Per-record, per-op iteration

Process records independently and operations in order. Make a deep copy of each record so you can safely mutate it without affecting the input or sibling records.

Step 2: Resolve the dot-path

Split op['path'] on . and walk the record dict to the parent container. For destructive ops (rename, remove, cast), if any segment is missing, skip the op silently. For add, use setdefault({}) at each step so missing intermediates are created.

Step 3: Apply the four op kinds

Dispatch on op['op']. rename: parent[op['new_name']] = parent.pop(leaf). add: parent[leaf] = op['default']. remove: del parent[leaf]. cast: convert parent[leaf] via the target type, but wrap it so a value that will not convert (None, a non-numeric string) is left untouched instead of aborting the whole run. Truncating '123.45' to an int means int(float(x)), not int(x).


The solution

Dot-path resolver with op dispatch
def migrate_records(records: list[dict], operations: list[dict]) -> list[dict]:
    import copy

    def cast_value(value, target_type):
        try:
            if target_type == 'int':
                return int(float(value))
            if target_type == 'float':
                return float(value)
            return str(value)
        except (ValueError, TypeError):
            return value

    out = []
    for record in records:
        rec = copy.deepcopy(record)
        for op in operations:
            *parents, leaf = op['path'].split('.')
            kind = op['op']
            if kind == 'add':
                container = rec
                for part in parents:
                    container = container.setdefault(part, {})
                container[leaf] = op['default']
                continue
            container = rec
            reached = True
            for part in parents:
                if not isinstance(container, dict) or part not in container:
                    reached = False
                    break
                container = container[part]
            if not reached or not isinstance(container, dict) or leaf not in container:
                continue
            if kind == 'rename':
                container[op['new_name']] = container.pop(leaf)
            elif kind == 'remove':
                del container[leaf]
            elif kind == 'cast':
                container[leaf] = cast_value(container[leaf], op['target_type'])
        out.append(rec)
    return out

Common follow-up questions