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

Use a stack and scan the string from left to right. Push each opening bracket; for every closing bracket, require the matching opener at the top of the stack and then pop it. The input is valid only if no mismatch occurs and the stack is empty at the end.

The standard stack algorithm

Balanced delimiters follow a last-in, first-out rule. The most recently opened bracket must be the first one closed. A Python list provides exactly the operations needed: append() pushes an opener and pop() removes the most recent opener.

Runnable 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

The function accepts a string and returns a Boolean. It raises ValueError for characters other than brackets because the implementation treats the input as a bracket sequence. That policy is deliberate: decide whether non-bracket text is invalid or should be ignored before choosing your version.

How each character is processed

  1. An opening bracket—(, [, or {—is appended to stack.
  2. A closing bracket looks up its required opener in matching.
  3. If the stack is empty, the closing bracket has nothing to close, so the function returns False.
  4. If the top stack item differs from the required opener, the nesting order is wrong and the function returns False.
  5. When the top item matches, it is popped.
  6. After the scan, not stack is true only when every opener has been closed.

Why the stack catches every invalid arrangement

Consider ([{}]). The stack evolves as [(], then [(, [], then [(, [, {]. The next character is }, which correctly matches {; the remaining closers then match [ and ( in reverse order.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

For ([)], the stack top is [ when ) arrives. Because ) requires (, the function fails immediately. For )(, the first character finds an empty stack. For ((, no mismatch occurs while scanning, but the final stack still contains two openers, so the result is false.

Expected results for common inputs

Input Result Reason
()[]{} True Each opener closes with the correct type.
([{}]) True Nested brackets close in reverse opening order.
(] False The closing bracket has the wrong type.
([)] False The closing order violates nesting.
)( False A closing bracket appears before any opener.
(( False Open brackets remain unmatched.
True An empty sequence is balanced under the usual definition.

Choose a policy for non-bracket characters

The strict function above raises an exception for input such as a(b). That is appropriate when a caller promises that the input contains only delimiters and you want malformed input reported explicitly. Text parsers usually need a different contract: ignore ordinary characters and validate only the brackets.

Ignoring ordinary text

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()

    return not stack

With this contract, valid_parentheses_in_text("a(b)[c]") returns True, while valid_parentheses_in_text("a(b]") returns False. Characters such as quotes, angle brackets, or Unicode symbols are not treated as delimiters unless you add them explicitly. If quoted strings can contain bracket characters, a real lexer must first determine whether those characters are inside a string literal; the simple function cannot infer that context.

List versus deque

A list is the clearest default because every operation happens at one end. Python’s list methods are designed to make a list a last-in, first-out stack. collections.deque is also valid and offers approximately constant-time appends and pops at either end. Choose it when the surrounding parser already needs efficient operations on both ends; it does not make this single-ended algorithm faster.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Criterion List stack deque
Bracket types Any types represented in the mapping Any types represented in the mapping
Non-bracket handling Strict or ignoring, depending on the loop Strict or ignoring, depending on the loop
Failure behavior Can return immediately on the first mismatch Can return immediately on the first mismatch
Time complexity O(n) O(n)
Worst-case auxiliary space O(n) O(n)
Readability for this task Usually simplest Useful when both ends are needed elsewhere

Complexity and memory behavior

Let n be the input length. The scanner examines each character at most once, performs constant-time dictionary and list operations, and stops as soon as it finds a mismatch. Its worst-case running time is O(n). A string containing only opening brackets can leave all n characters on the stack, so worst-case auxiliary space is O(n). Inputs that close quickly may use less memory, but the asymptotic bound remains O(n).

For very large data that arrives as a stream, keep the same stack and process chunks rather than concatenating the entire input. The state between chunks is simply the current stack; a mismatch still allows immediate termination. A final nonempty stack means the stream ended with unclosed brackets.

Testing the function

Small executable checks

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

for value, expected in cases.items():
    assert valid_parentheses(value) is expected, value

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

Useful edge cases

  • Test each supported bracket type by itself: (), [], and {}.
  • Test deep nesting to verify that the final stack is emptied correctly.
  • Test a close before an opener, such as ].
  • Test a wrong type at the deepest point, such as ({]}.
  • Test a missing final closer, such as {[.
  • Test ordinary text under both policies so callers know whether an exception or a Boolean is expected.

Troubleshooting common mistakes

Checking only the counts

Counting opening and closing characters is insufficient. (] has one opener and one closer but is invalid because the types differ. Always compare the closing character with the stack top.

Using the first opener instead of the latest

Removing with pop(0) or treating the stack as a queue breaks nested input. Use stack[-1] to inspect and stack.pop() to remove the latest opener.

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

Forgetting the empty-stack check

Evaluating stack[-1] before checking whether the stack is empty raises IndexError for inputs such as ). Keep not stack in the condition first, as in the implementation above.

Returning true after the loop unconditionally

An input such as (( never encounters a mismatched closer, yet it is invalid. Return not stack, not True.

Accidentally accepting unsupported symbols

If your application supports only three bracket types, do not silently classify angle brackets or other punctuation as valid. Either reject them in strict mode or ignore them under a clearly documented text policy.

Or skip the browser setup

If you are generating screenshots for documentation, test fixtures, or visual checks, ScreenshotNeo returns a screenshot or PDF from one GET request. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

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

Use the ScreenshotNeo API documentation for the complete option list. The API supports full-page captures with lazy images, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper sizes and page ranges, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector or network-idle waits, request and resource blocking, custom headers and cookies, user agents and authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of 100 URLs per call, a usage API, and an OpenAPI specification. Common parameter names from other screenshot APIs also work.

cURL

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)
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}`);

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing provides two months free. Create a free ScreenshotNeo account to get started.

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

FAQ

Does an empty string count as valid?

Yes. Under the conventional balanced-sequence definition, there are no unmatched brackets in an empty string, so the function returns True.

Can this validate Python syntax?

No. It checks delimiter nesting only. Python syntax also depends on names, indentation, operators, strings, comments, and grammar; use Python’s parser when syntax validation is required.

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

How can I support another delimiter pair?

Add the opener to the opening-character test and add its closing character and required opener to the mapping. Also update tests and document whether the new symbols may appear inside quoted text.

Frequently Asked Questions

Should I use a regular expression instead of a stack?

A regular expression is not a natural fit for arbitrary nesting. The stack directly represents the nesting state and has a predictable O(n) scan.

Can the validator report the position of the error?

Yes. Iterate with enumerate(text) and return or raise a result containing the current index when a mismatch occurs; retain the final length as the position for an unclosed opener.

Is the function safe to call from multiple threads?

Yes, when each call creates its own local stack as shown. Do not share a mutable stack between calls unless you add synchronization and intentionally maintain shared parser state.

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.