ScreenshotNeo

BlogHow-to

How to Check for Valid Parentheses in Python

Learn the stack algorithm for validating (), [], and {} in Python, with edge cases, tests, complexity, and production-ready variants.

By the ScreenshotNeo team1 October 20266 min read

Use a last-in, first-out stack. Scan the string from left to right, push every opening bracket, and require each closing bracket to match the most recently opened bracket. The input is valid only if no mismatch occurs and the stack is empty at the end.

def valid_parentheses(text: str) -> bool:
    matching = {")": "(", "]": "[", "}": "{"
    }
    stack: list[str] = []

    for char in text:
        if char in "([{":
            stack.append(char)
        elif char in matching:
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()
        else:
            raise ValueError(f"unexpected character: {char!r}")

    return not stack

This version treats an empty string as valid and rejects every non-bracket character. If your contract allows ordinary text such as a(b), choose the permissive variant below instead.

How the stack algorithm works

  1. Read characters from left to right.
  2. Push (, [, or { onto the stack.
  3. For ), ], or }, fail if the stack is empty or its top item is not the corresponding opener.
  4. Otherwise pop the matching opener.
  5. After the scan, return True only when the stack is empty.

Nested brackets must close in reverse order. For ([{}]), the final opener is {, so } must appear next. In ([)], the stack top is [ when ) arrives, so validation stops immediately.

Complete runnable example

from __future__ import annotations


def valid_parentheses(text: str) -> bool:
    """Return whether text contains correctly nested bracket characters.

    This strict version raises ValueError for non-bracket characters.
    """
    matching = {")": "(", "]": "[", "}": "{"
    }
    stack: list[str] = []

    for char in text:
        if char in "([{":
            stack.append(char)
        elif char in matching:
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()
        else:
            raise ValueError(f"unexpected character: {char!r}")

    return not stack


examples = {
    "()[]{}": True,
    "([{}])": True,
    "(]": False,
    "([)]": False,
    ") (".replace(" ", ""): False,
    "((': False,
    "": True,
}

for value, expected in examples.items():
    actual = valid_parentheses(value)
    print(f"{value!r}: {actual}")
    assert actual is expected

Python lists are suitable stacks: append() pushes and pop() removes the top item. The Python tutorial documents this last-in, first-out use of lists (official list-stack documentation).

Decide how to handle non-bracket characters

Strict validation

Use the first implementation when the entire input must consist of brackets. Raising ValueError exposes malformed input instead of silently accepting it.

Ignore other characters

def valid_parentheses_in_text(text: str) -> bool:
    matching = {")": "(", "]": "[", "}": "{"
    }
    stack: list[str] = []

    for char in text:
        if char in "([{":
            stack.append(char)
        elif char in matching:
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()
        # Letters, digits, spaces, and punctuation are ignored.

    return not stack

With this policy, "a(b)[c]" is valid and "a([)]" is invalid. Document the policy in your function or API contract because it changes the result for mixed text.

Validate only selected bracket types

def valid_round_parentheses(text: str) -> bool:
    depth = 0
    for char in text:
        if char == "(":
            depth += 1
        elif char == ")":
            depth -= 1
            if depth < 0:
                return False
        else:
            raise ValueError(f"unexpected character: {char!r}")
    return depth == 0

For one bracket type, a counter is enough. For multiple types, use a stack because a counter cannot detect type or nesting-order errors.

Examples and failure modes

Input Result Reason
()[]{} True All pairs match.
([{}]) True Proper nesting.
(] False Wrong bracket type.
([)] False Wrong closing order.
)( False Closing bracket appears with an empty stack.
(( False Opening brackets remain.
"" True No unmatched brackets.

Testing the validator

import pytest


def test_valid_sequences():
    for value in ("", "()", "[]{}", "([{}])"):
        assert valid_parentheses(value)


def test_invalid_sequences():
    for value in ("(", ")", "(]", "([)]", ")("):
        assert not valid_parentheses(value)


def test_strict_mode_rejects_text():
    with pytest.raises(ValueError):
        valid_parentheses("a(b)")

Useful additional cases include a single opener or closer, very deep nesting, repeated pairs, Unicode characters in permissive mode, and a mismatch at the final character. Property-based tests can generate random balanced strings and mutate one bracket to verify rejection.

Complexity, memory, and implementation choices

  • Time: O(n), because each character is inspected once.
  • Auxiliary space: O(n) in the worst case, when every character is an opener.
  • Early failure: A mismatch or premature close returns immediately, which avoids scanning the remainder.
  • List versus deque: A list is clearest when pushing and popping at one end. collections.deque also provides approximately O(1) appends and pops at either end, as described in the official deque documentation.
from collections import deque


def valid_with_deque(text: str) -> bool:
    matching = {")": "(", "]": "[", "}": "{"
    }
    stack: deque[str] = deque()
    for char in text:
        if char in "([{":
            stack.append(char)
        elif char in matching:
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()
    return not stack

Production considerations

  • Define whether empty input is valid before exposing the function in an API.
  • Define whether non-bracket characters are rejected, ignored, or handled by a separate lexer.
  • Limit input length when strings come from untrusted clients; the stack can grow to O(n).
  • Do not use repeated string replacement such as removing () until nothing changes. That approach can rescan the input many times and is harder to reason about.
  • For quoted strings, comments, or escaped brackets, first tokenize those constructs. A simple bracket scan cannot know that "(" is inside a string literal.

Troubleshooting

Symptom Cause Fix
a(b) raises ValueError The strict function rejects non-bracket characters. Use the permissive implementation or filter input intentionally.
([)] is accepted The implementation tracks only a count, not bracket types. Use a stack with a mapping from closing to opening brackets.
( is accepted The function forgot the final emptiness check. Return not stack after scanning.
) causes an index error The code reads stack[-1] before checking whether the stack is empty. Check not stack first.
Only parentheses are required, but brackets fail The accepted-character policy is narrower than the input. Add the required opener and closer pairs to the mapping.
Brackets inside strings are counted The input is source code or quoted text, not a raw bracket sequence. Tokenize strings and comments before validating delimiters.

Or skip the browser setup

If your workflow also needs screenshots of documentation, test pages, or rendered results, ScreenshotNeo provides a single screenshot API request instead of maintaining browser automation. It removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; and its MCP server lets AI agents take screenshots.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);

ScreenshotNeo supports PNG, JPEG, WebP, and PDF output, plus full-page and element capture, custom CSS and JavaScript, waits, blocking rules, device presets, caching, signed links, async jobs, bulk capture, and usage reporting. Its response includes X-Page-Verdict and X-Billed headers so you can see what happened. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Is an empty string valid?

Yes, under the usual balanced-sequence definition, because it contains no unmatched bracket.

Can I validate only ()?

Yes. A depth counter is simpler for one bracket type; use a stack for multiple types.

Why does a counter fail for (]?

A counter records quantity but not bracket type. The stack preserves the opener that each closer must match.

Should I use a regular expression?

For arbitrary nesting, no. A stack directly models the required last-in, first-out relationship and runs in one pass.

How do I validate brackets in Python source code?

Lex strings, comments, and escaped characters first. Then apply the stack to the remaining delimiter tokens.