Skip to main content

How to Check for Balanced Parentheses in Python

Validating whether a string has balanced parentheses, where every opening bracket has a matching closing bracket in the correct order, is a fundamental computer science problem. It's used in compilers, math expression evaluators, and syntax validators.

A string is balanced if:

  1. Every opening bracket has a corresponding closing bracket.
  2. Brackets are closed in the correct order (properly nested).

The only robust solution that handles multiple bracket types and proper nesting is the Stack algorithm.

Understanding the Stack Algorithm

A Stack follows the LIFO (Last-In, First-Out) principle:

  1. When encountering an opener ((, {, [), push it onto the stack
  2. When encountering a closer (), }, ]), check the top of the stack:
    • If it matches the corresponding opener, pop the stack
    • If it doesn't match (or stack is empty), the string is invalid
  3. After processing all characters, the string is valid only if the stack is empty

Basic Implementation

def is_balanced(expression: str) -> bool:
"""Check if parentheses in expression are balanced."""
stack = []

# Map each closer to its corresponding opener
pairs = {
')': '(',
'}': '{',
']': '['
}

for char in expression:
if char in pairs.values():
# It's an opener - push to stack
stack.append(char)

elif char in pairs:
# It's a closer - check for match
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()

# Valid only if all openers were closed
return len(stack) == 0


# Test cases
print(is_balanced("{[()]}")) # True
print(is_balanced("([)]")) # False (wrong nesting)
print(is_balanced("((()")) # False (unclosed)
print(is_balanced("")) # True (empty is valid)

Output:

True
False
False
True

Detailed Trace Example

For input {[()]}:

StepCharActionStack
1{Push opener['{']
2[Push opener['{', '[']
3(Push opener['{', '[', '(']
4)Pop (matches ()['{', '[']
5]Pop (matches [)['{']
6}Pop (matches {)[]
EndStack empty✅ Valid

Why Simple Counting Fails

A common beginner mistake is counting brackets:

# ❌ WRONG APPROACH
def is_balanced_wrong(s: str) -> bool:
return s.count('(') == s.count(')')
Counting Doesn't Detect Nesting Errors

The string ([)] has equal counts of each bracket type, but it's invalid because:

  • The [ is incorrectly closed by )
  • The ( is incorrectly closed by ]

Only a stack can detect this ordering violation.

Enhanced Implementation with Error Details

Return specific information about what went wrong:

def validate_brackets(expression: str) -> dict:
"""Validate brackets and return detailed results."""
stack = []
pairs = {')': '(', '}': '{', ']': '['}
openers = set(pairs.values())

for i, char in enumerate(expression):
if char in openers:
stack.append((char, i))

elif char in pairs:
if not stack:
return {
'valid': False,
'error': f"Unexpected '{char}' at position {i}",
'position': i
}

top_char, top_pos = stack[-1]
if top_char != pairs[char]:
return {
'valid': False,
'error': f"Mismatched '{top_char}' at {top_pos} with '{char}' at {i}",
'position': i
}
stack.pop()

if stack:
unclosed = stack[-1]
return {
'valid': False,
'error': f"Unclosed '{unclosed[0]}' at position {unclosed[1]}",
'position': unclosed[1]
}

return {'valid': True, 'error': None, 'position': None}


# Test with errors
print(validate_brackets("([)]"))
print(validate_brackets("((()"))

Output:

{'valid': False, 'error': "Mismatched '[' at 1 with ')' at 2", 'position': 2}
{'valid': False, 'error': "Unclosed '(' at position 1", 'position': 1}

Handling Mixed Content

Real-world strings contain more than just brackets. Ignore non-bracket characters:

def is_balanced_expression(expression: str) -> bool:
"""Check balance in expressions with mixed content."""
stack = []
pairs = {')': '(', '}': '{', ']': '['}
openers = set(pairs.values())

for char in expression:
if char in openers:
stack.append(char)
elif char in pairs:
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()
# Ignore all other characters

return len(stack) == 0


# Works with real expressions
print(is_balanced_expression("func(arr[0], {key: value})")) # True
print(is_balanced_expression("if (x > 0) { return arr[i]; }")) # True
print(is_balanced_expression("print('Hello (World'")) # False

Validating Code Blocks

Check if code has balanced braces:

def validate_code_blocks(code: str) -> bool:
"""Validate balanced brackets in code, ignoring strings."""
stack = []
pairs = {')': '(', '}': '{', ']': '['}
openers = set(pairs.values())

in_string = False
string_char = None

for i, char in enumerate(code):
# Handle string literals (skip their contents)
if char in ('"', "'") and (i == 0 or code[i-1] != '\\'):
if not in_string:
in_string = True
string_char = char
elif char == string_char:
in_string = False
continue

if in_string:
continue

# Normal bracket checking
if char in openers:
stack.append(char)
elif char in pairs:
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()

return len(stack) == 0


code = '''
def example():
data = {"key": "value with (parentheses)"}
return data["key"]
'''
print(validate_code_blocks(code)) # True

Using deque for Performance

For very long strings, collections.deque is slightly faster:

from collections import deque


def is_balanced_deque(expression: str) -> bool:
"""Balanced check using deque for better performance."""
stack = deque()
pairs = {')': '(', '}': '{', ']': '['}

for char in expression:
if char in pairs.values():
stack.append(char)
elif char in pairs:
if not stack or stack[-1] != pairs[char]:
return False
stack.pop()

return len(stack) == 0

Common Test Cases

InputExpectedReason
()TrueSimple match
()[]{}TrueSequential pairs
{[()]}TrueNested correctly
([)]FalseCrossed nesting
(((FalseUnclosed openers
)))FalseNo matching openers
""TrueEmpty string is valid
({})TrueMixed nesting

Summary

  • Use a Stack (LIFO) to track opening brackets.
  • Push openers, pop and verify on closers.
  • Simple counting fails to detect nesting order violations.
  • The stack must be empty at the end for valid input.
  • Consider ignoring non-bracket characters for real-world expressions.