tag-expressions

A small boolean-expression parser and evaluator for selecting Gherkin scenarios by tag, used by behave and other Cucumber-family test runners.

Library
PyPI
v11.0.1
20stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
70/100Good
Development Activity100
Maintenance84
Community28
Maturity56
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
78/100Good
Architecture80
Code Quality85
Innovation55
Learning Curve90

cucumber-tag-expressions is the official Python implementation of Cucumber’s Tag Expression language, a boolean query syntax (and, or, not, parentheses) for selecting which Gherkin features and scenarios a BDD test run should include. It replaces the older comma/tilde tag syntax with a single readable expression such as @smoke and not @slow, letting test runners like behave filter large scenario suites without brittle custom logic.

The library is intentionally tiny and dependency-free: a tokenizer, a Shunting Yard parser, and a handful of expression-tree node classes (And, Or, Not, Literal) that evaluate against a set of tags. It is one of several polyglot ports maintained in the same cucumber/tag-expressions monorepo (Go, Java, JavaScript, Perl, PHP, Ruby, and .NET all ship equivalent implementations), so behavior is kept consistent across the Cucumber ecosystem through a shared testdata suite.

Because it exposes just a parse() function and a couple of exception/model classes, it’s meant to be embedded inside a test runner rather than used standalone — the parsed expression is a callable object that takes a set of tag strings and returns a boolean.

What You Get

  • Tag expression parser - parse("@smoke and not @slow") builds an evaluation tree using a Shunting Yard implementation with correct operator precedence and associativity.
  • Callable expression objects - the parsed tree is directly callable against a set of tag strings, e.g. expression({"@smoke", "@wip"}) returns True/False.
  • Escape handling - reserved characters (, ), \, and whitespace inside a tag can be escaped with \, so tags like @x(y) round-trip correctly.
  • Detailed syntax errors - TagExpressionError messages point at the exact token position in the original expression string, not just a generic parse failure.
  • Cross-language consistency - the parser’s grammar and edge cases are validated against the same shared testdata used by the Go, Java, JavaScript, Ruby, PHP, Perl, and .NET ports in the same monorepo.

Common Use Cases

  • Filtering scenarios in behave - a BDD test runner passes --tags "@fast and not @flaky" to select a scenario subset for a CI run.
  • Conditional hooks - a before_scenario/after_scenario hook checks a tag expression to decide whether it should run for the current scenario.
  • Migrating legacy tag filters - projects moving off comma/tilde tag syntax (--tags @foo,@bar) adopt tag expressions ("@foo or @bar") for clearer boolean intent.
  • Building custom Gherkin tooling - any Python tool that needs to evaluate Cucumber-style tag selection logic (test dashboards, scenario linters, CI gating scripts) can reuse the same parser instead of reimplementing it.

Under The Hood

Architecture The package is a small, self-contained two-module design: parser.py implements tokenization and a classic Shunting Yard algorithm (via the TagExpressionParser class and a Token enum encoding keyword, precedence, and associativity) that turns a tag-expression string into a tree of model.py node objects (And, Or, Not, Literal, True_), each of which implements evaluate(values)/__call__ so the parsed result is directly usable as a predicate over a set of tags. There is no runtime state beyond the parse tree itself, and the public surface is limited to parse(), TagExpressionParser, and TagExpressionError, re-exported from __init__.py — a design that keeps the library trivially embeddable inside a larger test runner.

Tech Stack Pure Python 3.10+ with zero runtime dependencies. The build uses uv_build as its PEP 517 backend and uv.lock for dev dependencies (ruff, pytest, coverage, pyyaml). Linting is configured through ruff with an extensive rule set (bugbear, comprehensions, security, naming, performance, and more) rather than a separate flake8/black/isort stack.

Code Quality Testing is comprehensive relative to the library’s size: dedicated unit tests for the tokenizer/parser (test_parser.py) and expression-tree evaluation (test_model.py), functional tests exercising the public parse() API end-to-end, and a data-driven suite (test_errors.py, test_evaluations.py, test_parsing.py) that runs the same behavioral cases used across the project’s other language ports. The README’s own code examples are wired into pytest via --doctest-glob=README.md, so documentation and behavior can’t drift apart silently. Error handling favors explicit, position-aware TagExpressionError exceptions over silent failures, and CI runs a dedicated test-python.yml workflow plus CodeQL scanning on every change.

What Makes It Unique Rather than being a standalone project, this is the Python arm of a deliberately polyglot specification: the same tag-expression grammar and edge-case test data are shared across seven language implementations in one monorepo, so the Python behavior is kept in lockstep with Cucumber’s Go, Java, JavaScript, Ruby, PHP, Perl, and .NET runners. That cross-language contract, rather than any single clever algorithmic trick, is what the project optimizes for.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search