# Closing Time

> Every bracket you open is a promise. Check that the parser keeps all of them.

Canonical URL: <https://datadriven.io/problems/balanced-bracket-gate>

Domain: Python · Difficulty: easy · Seniority: junior

## Problem

Our query layer gates filter expressions before the parser sees them: each expression groups conditions with round, square, and curly brackets, and the gate confirms every opener is closed by the matching kind in the right order. Bracket characters inside a single-quoted string value are literal data, so they are ignored along with all other non-bracket characters. Return whether the brackets are balanced; an expression with no brackets is balanced.

## Worked solution and explanation

### Why this problem exists in real interviews

Strip the query-gate costume and this is a per-character LIFO match: the last bracket you opened must be the first one you close, and closed by the same kind. Most people who have not internalized that reach for counting, tallying opens against closes per type. It passes for ([]) and fails silently for ([)], where the counts balance perfectly but the nesting is garbage. The twist that punishes a memorized answer: a bracket sitting inside a quoted value like 'a(b]' is data the user typed, not structure, so a solution that matches every bracket it sees both green-lights nonsense and rejects valid input.

---

### Why counting is not enough

**Counting (broken)**

Keep a tally per type and require each to end at zero. On a[b(c]d) you count one of each opener and one of each closer, the tallies zero out, and you wrongly return True. Counting throws away the one thing that matters: the ORDER unclosed brackets are waiting in.

**Stack (correct)**

Push every opener. On each closer, the only valid match is whatever is on top of the stack. For a[b(c]d the ] arrives while ( is on top, the kinds disagree, and you reject immediately. The stack remembers order, so interleaving cannot sneak through.

---

### Break down the requirements

#### Step 1: Map each closer to its opener

A dict {')':'(', ']':'[', '}':'{'} turns the three bracket pairs into a single lookup. This also gives you two cheap membership tests: a character is an opener if it is one of the dict values, a closer if it is one of the dict keys.

#### Step 2: Track whether you are inside a quoted value

A single quote flips an in-string flag. While that flag is set, skip every character, brackets included, because those characters are text the user typed rather than structure. This flag is exactly what a recalled bracket matcher lacks: it has no notion that 'x(y' is data, so it tries to match the ( and corrupts the answer.

#### Step 3: Ignore everything that is not a bracket

Outside of quotes the expression still carries identifiers, commas, and operators. Walk character by character and simply skip anything that is neither an opener nor a closer. The non-bracket noise is irrelevant, but the bracket ORDER is not, which is why a counting hack feels tempting and is still wrong.

#### Step 4: Push openers, match closers against the top

On an opener, push it. On a closer, the stack must be non-empty AND its top must be the matching opener; otherwise return False right away. A closer with an empty stack means something closed that was never opened.

#### Step 5: Demand an empty stack at the end

Reaching the end is not success on its own. A leftover opener like count(items[0] means a promise never kept, so the answer is balanced only when the stack is empty. An input with no brackets at all leaves the stack empty and correctly returns True.

---

### The solution

**Single pass with a stack**

```python
def brackets_balanced(expr):
    pairs = {')': '(', ']': '[', '}': '{'}
    openers = set(pairs.values())
    stack = []
    in_string = False
    for ch in expr:
        if ch == "'":
            in_string = not in_string
        elif in_string:
            continue
        elif ch in openers:
            stack.append(ch)
        elif ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
    return not stack
```

> **Complexity**
>
> Time is O(n): one pass over the string with O(1) dict lookups and list push/pop, plus a boolean flag toggled on each quote. Space is O(n) worst case, when the input is all openers and the stack grows to the full length. At config-validation scale (expressions of a few hundred characters) this is instant, and because it streams character by character it behaves the same on a megabyte template.

> **Common Pitfall**
>
> Calling stack.pop() before checking the stack is non-empty. On a leading closer like value) that raises IndexError instead of returning False. The short-circuit `not stack or stack.pop() != pairs[ch]` checks emptiness first, so the pop never runs on an empty stack.

> **Interviewers Watch For**
>
> Whether you reject the moment a closer mismatches rather than scanning the whole string first, whether you remember the final empty-stack check that catches a trailing opener, and whether you spot that brackets inside a quoted value are data rather than structure. Naming the ([)] counterexample and the quoted-bracket case unprompted is the senior tell.

---

## Common follow-up questions

- What if a quoted value can itself contain an escaped quote, so the real closing quote is the one not preceded by a backslash? _(Tests tracking escape state instead of blindly toggling a flag on every quote.)_
- What if the gate must also report WHERE the first mismatch occurs, not just whether the string is valid? _(Tests carrying the index alongside each pushed opener so you can return a position, not a bool.)_
- If these expressions stream in from a network socket too large to hold in memory, how does the approach change? _(Tests that a stack works incrementally, so you process chunks while keeping only the unclosed-opener stack plus the in-string flag as state.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/balanced-bracket-gate)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). 100% free data engineering interview prep. Live code execution against Postgres 16, Python 3.11, and Spark sandboxes. No paywall, no premium tier, no signup gate.