A friend of mine recently went for a coding interview and was given a small but tricky challenge: validate if a block of Python code has the correct indentation.
The catch? You cannot run the code. You are not supposed to validate syntax either. You just need to tell if the indentation structure is valid.
We were chatting and I thought β actually, this is a nice little puzzle. So I decided to write a validator in TypeScript just for fun.
This is a structural check only. We do not validate Python syntax or runtime behaviour. Just indentation.
To keep things simple and deterministic:
:
should be followed by a more indented linenumber = 5
if number > 0:
print("Positive number")
if number % 2 == 0:
print("Even number")
else:
print("Odd number")
:
number = 5
if number > 0:
print("Positive number") # not indented
number = 5
if number > 0:
print("Positive number")
print("This line has wrong indentation") # 2 spaces instead of 4
number = 5
if number > 0:
print("Positive number")
print("Too much indentation") # 8 spaces when only 4 is valid
number = 5
if number > 0:
if number % 2 == 0:
print("Missing indentation") # should be indented with 8 spaces
When reading the requirement, my thought process was pretty straightforward:
:
), expect an indent increase on the next line. This is different from the valid parenthesis problem where itβs more straightforward - if the string starts with an opening bracket ({
, [
, or (
), you know exactly what the expected closing bracket (}
, ]
, or )
) should beLetβs start with the basic structure:
const TAB_SIZE = 4
export function validatePythonIndentation(input: string): boolean {
if (!input) {
return false;
}
const indents: number[] = [0];
const lines = input.split('\n');
for (const rawLine of lines) {
const line = rawLine.trimEnd();
// DO THE REAL STUFF
}
}
function countSpacesFromBeginning(line: string): number {
return line.length - line.trimStart().length;
}
function isLineStartNewBlock(line: string): boolean {
return line.endsWith(':');
}
Now we add the logic to handle indentation nesting:
indents
):
, push a new expected indentation (current + 4 spaces) to the stackThe tricky part is the pop loop that handles dedents:
const TAB_SIZE = 4
export function validatePythonIndentation(input: string): boolean {
if (!input) {
return false;
}
+ // Start with [0] because the first line should have no indentation
+ // This represents the base level of indentation (0 spaces)
+ const indents: number[] = [0];
const lines = input.split('\n');
for (const rawLine of lines) {
const line = rawLine.trimEnd();
if (line === '') {
continue // skip empty line;
}
const currentIndent = countSpacesFromBeginning(line);
+ while (indents.length && currentIndent < indents[indents.length - 1]) {
+ indents.pop();
+ }
const expectedIndent = indents[indents.length - 1];
if (currentIndent !== expectedIndent) {
return false;
}
if (isLineStartNewBlock(line)) {
const nextExpectedIndent = currentIndent + TAB_SIZE;
indents.push(nextExpectedIndent);
}
}
+ return true;
}
function countSpacesFromBeginning(line: string): number {
return line.length - line.trimStart().length;
}
function isLineStartNewBlock(line: string): boolean {
return line.endsWith(':');
}
What is going on here?
[0]
Why return true?
Our validator checks every line. If no line violates the expected indent, the input is valid.
So return true at the end means:
I saw no mistake, so I approve.
Letβs walk through this Python snippet using our indentation validator:
number = 5
if number > 0:
if number % 2 == 0:
print("Even")
else:
print("Odd")
print("done")
We assume TAB_SIZE = 4
.
indents = [0]
Starts with 0
to mean top-level code should have no indentation.number = 5
:
β no new blockindents = [0]
(unchanged)if number > 0:
:
β push 0 + 4 = 4
indents = [0, 4]
if number % 2 == 0:
:
β push 4 + 4 = 8
indents = [0, 4, 8]
print("Even")
:
β no pushindents = [0, 4, 8]
(unchanged)else:
4
indents = [0, 4]
:
β push 4 + 4 = 8
indents = [0, 4, 8]
print("Odd")
:
β no pushindents = [0, 4, 8]
print("done")
:
β doneindents = [0]
So at the end, we never returned false
. That means the indentation is valid, so we return true
.
Try it out in the TypeScript Playground.