def update_path(dct, path, update):
    """
    Update a dict structure (e.g. JSON), using a path to specify what value to update.

    The `path` specifies a hierarchy of (string) dictionary keys to traverse, separated by periods.
    Suffixing a key with `[]` indicates the value of that key is an list, and should be iterated.

    e.g. `foo.bar[].baz`

    The `update` can be simply the new value, or a function that takes the old value and returns the
    new value.

    Dicts and lists along the path are copied before being updated, and the updated dict structure
    is returned.
    """

    if not isinstance(dct, dict):
        return dct

    if callable(update):
        update_fn = update
    else:
        def update_fn(value):
            return update

    field, sub_path = path.split('.', 1) if '.' in path else (path, None)

    array_field = False
    if field.endswith('[]'):
        field = field[:-2]
        array_field = True

    if field not in dct:
        return dct

    value = dct[field]

    if array_field and not isinstance(value, list):
        return dct

    if sub_path:
        if array_field:
            updated_value = [update_path(v, sub_path, update_fn) for v in value]
        else:
            updated_value = update_path(value, sub_path, update_fn)

    else:
        if array_field:
            updated_value = [update_fn(v) for v in value]
        else:
            updated_value = update_fn(value)

    updated_dct = dct.copy()
    updated_dct[field] = updated_value
    return updated_dct