The Forward Fill
An easy Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- easy
- Seniority
- L3
- Asked in
- Meta
Problem
Given a list that may contain None values, return a list of the same length where each None is replaced by the most recent prior non-None value. If a None appears before any non-None value has been seen, leave it as None.
Patch the gaps in a noisy sensor stream.
Example
Input:
values = [1,null,null,2,null]
Output:
[1,1,1,2,2]
Worked solution and explanation
What this really is
Strip off the "noisy sensor stream" costume and this is a stateful scan that carries one value forward: at each position you either see a real reading or a gap, and a gap inherits whatever the last real reading was. Everyone can write a loop. The thing that actually separates candidates is realizing the fill source is the last value you EMITTED, not the element physically before you in the input, and that a run of gaps at the very start has nothing to carry. Reach backward one index instead of tracking state and you either IndexError on the first element or, worse, copy None around silently and 'fill' leading gaps with garbage.
How to reason to it
Step 1: Decide where the fill value comes from
It comes from the OUTPUT stream, not the raw input. Consecutive gaps must all inherit the same carried value, so [1, None, None] becomes [1, 1, 1]. If you instead looked at values[i-1], the second gap would read the first gap (still None on a raw read) and break the run. Tracking last_valid in a variable sidesteps this entirely.
Step 2: Handle the leading-gap case for free
Initialize last_valid to None. A None before any real reading appends None, which is exactly the spec. There is no special 'first element' branch to write; the initial state already encodes 'nothing to carry yet'.
Step 3: Emit exactly one value per input element
Append inside the loop body every iteration, after possibly updating last_valid. This guarantees the output length equals the input length and that real values pass through untouched. Empty input yields an empty list with no special-casing.
def forward_fill(values: list) -> list:
result = []
last_valid = None
for val in values:
if val is not None:
last_valid = val
result.append(last_valid)
return resultOne pass, one state variable, no index arithmetic.
- Look-back by index (fragile)
- for i in range(len(values)): if values[i] is None: result.append(values[i-1]) else: result.append(values[i]) On i=0 this reads values[-1] (wraps to the LAST element in Python, a silent bug), and a run of gaps reads raw None neighbors, so [1, None, None] returns [1, 1, None].
- Carry a state variable (correct)
last_valid= None for val in values: if val is not None:last_valid= val result.append(last_valid) The carry always reflects the last EMITTED real value, so gap runs fill cleanly and the leading-gap case falls out of the None initializer.
Common follow-up questions
- How would you cap the fill so no more than k consecutive gaps get filled? (Tests adding a run-length counter that resets on each real value and stops carrying once it exceeds k.)
- Now do a backward fill instead, and then interpolate linearly between known points. (Tests iterating in reverse and, for interpolation, tracking both bracketing known values plus the distance between them.)
- How does pandas implement fillna(method='ffill') on a column, and why is it faster than this loop at scale? (Tests awareness that the same carry logic runs vectorized in C over a contiguous buffer rather than a Python-level loop.)