Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a last-in, first-out stack. Scan the string from left to right, push each opening bracket, and require every closing bracket to match the opener currently on top of the stack. A closing bracket with no opener, a wrong bracket type, or any opener left at the end makes the string invalid.

The implementation below validates (), [], and {} in one pass. It also makes the policy for non-bracket characters explicit, which is essential when inputs such as a(b) are possible.

The stack algorithm

Balanced delimiters are nested, so the most recently opened bracket must be the first one closed. That is exactly last-in, first-out behavior. Python’s list type provides clear append() and pop() operations for this purpose.

  1. Read characters from left to right.
  2. Push (, [, or { onto the stack.
  3. For ), ], or }, check that the stack is nonempty and that its top is the corresponding opener.
  4. Return False immediately for an empty stack or a mismatch; otherwise pop the opener.
  5. After the scan, return True only if the stack is empty.

A complete Python implementation

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 function accepts a string containing only brackets, or a string in which non-bracket characters are treated as invalid. That choice is deliberate. If your input contract allows ordinary text, change the final else branch to continue (or simply do nothing) so that letters, spaces, and punctuation are ignored.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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()
        # Other characters are intentionally ignored.

    return not stack

Why each failure case is detected

Wrong bracket type

For (], the stack contains ( when ] arrives. The mapping says that ] must close [, so the function returns False.

Wrong nesting order

For ([)], the stack is ['(', '['] when ) is read. The top is [, not (, so the crossing close is rejected. Properly nested text such as ([{}]) always closes the top item first.

Closing before opening

For )(, the first character is a close while the stack is empty. Returning immediately prevents an invalid access and identifies the error at the earliest possible position.

Unclosed openings

For ((, no mismatch occurs during the scan, but two openers remain. return not stack therefore returns False.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Empty input

The empty string returns True under the usual definition of a balanced sequence: it contains no unmatched brackets. If your application requires at least one bracket, add a separate nonempty-input check.

Examples and expected results

Input Result Reason
()[]{} True Every opener is closed by the matching type.
([{}]) True Nested closures occur in reverse opening order.
(] False The closing type does not match.
([)] False The nesting order crosses.
)( False A close appears with no opener.
(( False Openers remain after scanning.
"" True Empty sequences are balanced by the standard definition.

For the strict function, valid_parentheses("a(b)") raises ValueError. For the text-oriented version, valid_parentheses_in_text("a(b)") returns True. Choose one contract and document it at the boundary of your program.

Complexity and data-structure choice

With n characters, the scan takes O(n) time because each character is examined once. The stack uses O(n) auxiliary space in the worst case, when every character is an opener. It uses less space for mostly closed or non-bracket text.

A list is the default recommendation because all operations happen at one end and the intent is obvious. collections.deque is also valid and provides approximately O(1) appends and pops at either end. Use a deque when the surrounding parser already needs efficient operations on both ends; it does not make this one-ended algorithm faster or simpler.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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()
        else:
            raise ValueError(f"unexpected character: {char!r}")

    return not stack

Testing the validator

Test successful nesting, each failure mode, and the input policy. A small table-driven test catches regressions without obscuring the algorithm.

cases = {
    "()[]{}": True,
    "([{}])": True,
    "(]": False,
    "([)]": False,
    ")(": False,
    "(": False,
    "": True,
}

for source, expected in cases.items():
    actual = valid_parentheses(source)
    assert actual == expected, (source, actual, expected)

try:
    valid_parentheses("a(b)")
except ValueError:
    pass
else:
    raise AssertionError("strict mode should reject non-bracket characters")

Useful properties for larger test suites

  • Adding a correctly matched pair around a valid string should preserve validity.
  • Changing one closing bracket to a different type should make the result invalid.
  • Removing one bracket from a valid string should leave an unmatched opener or closer.
  • Very long runs of opening brackets should complete without recursion, because the algorithm is iterative.

Common implementation mistakes

Checking only the counts

Counting opening and closing brackets is insufficient. (] has one opener and one closer but is not valid; order and type must be checked with a stack.

Using the first opener instead of the latest

Removing from the front of a list tests the wrong nesting rule and can also make operations needlessly expensive. Use stack[-1] and stack.pop().

Forgetting the final stack check

A scan that rejects mismatches but always returns True incorrectly accepts ( and ((. The final emptiness check is mandatory.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Indexing an empty stack

Check not stack before reading stack[-1]. This handles premature closing brackets safely.

Silently choosing a character policy

Ignoring non-bracket characters may be right for source-code fragments, while rejecting them may be right for a delimiter-only parser. Neither policy is universally correct; expose it through separate functions or an explicit option.

Adapting the function for real parsers

Returning an error position

For diagnostics, return the index and a message instead of only a Boolean. The same loop can report the position of a premature close or mismatch; after the loop, any remaining opener can be reported as unclosed. Keep the Boolean function for callers that only need acceptance.

Supporting additional delimiter pairs

Extend the opening-character test and the matching dictionary together. Every closing symbol must map to exactly one opener. If delimiters can be escaped or occur inside quoted strings, handle those lexical rules before applying the simple stack algorithm; quotes and escape sequences are not parentheses and cannot be safely ignored by this function.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Streaming input

The loop only needs the current character and the stack, so it can be moved into a function that consumes chunks from a file or network stream. Preserve the same stack between chunks and perform the emptiness check only after the final chunk.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is to capture a webpage rather than validate delimiters, ScreenshotNeo is the first service to try: it removes consent banners, popups, and chat widgets before capture, bills only clean shots, and has a $5 paid plan for 3,000 shots.

One GET request returns a PNG, JPEG, WebP, or PDF. The API also reports page and billing outcomes in X-Page-Verdict and X-Billed headers.

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

Python:

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)

Node.js:

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(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

See the parameter reference and all 63 capture options in the ScreenshotNeo documentation. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Cookie banners, newsletter popups, and chat widgets can be removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. The Free plan includes 1,000 shots per month with no card, and paid plans start at $5 for 3,000 shots. Sign up free.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When this algorithm is not enough

The validator checks delimiter balance only. It does not parse Python syntax, determine whether a bracket appears inside a string literal, or understand comments. For complete Python source validation, use Python’s parser after any preprocessing your application requires. Keep this small stack routine for token streams, configuration formats, interview exercises, and other contexts where bracket characters are the defined input.

Frequently Asked Questions

Should an empty string be considered valid?

Yes, under the standard balanced-sequence definition. Add a separate nonempty check only when your application’s input contract requires at least one bracket.

Why not solve this with a regular expression?

Arbitrarily nested pairs require remembering an unbounded sequence of openers. A stack directly models that requirement and reports mismatches as soon as they occur.

Can I validate only parentheses instead of three bracket types?

Yes. Replace the opening-character test with if char == '(' and the mapping with {')': '('}; the push, match, pop, and final-empty checks remain the same.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.