Skip to main content

“An inscribed or painted symbol considered to have magical power.”

Sigils is a Python library for text and meta-text interpolation. It provides context-based interpolation, function execution, nested and recursive interpolation, ambient execution context, built-in transformation tools, and a command-line interface.

The maintained PyPI distribution is named gway-sigils. The Python package and command-line interface remain named sigils, so existing imports and runtime usage are unchanged.

Any Python object can be provided as explicit context, including nested dictionaries, lists, and functions. Sigils supports Python 3.11 and newer and uses the standard-library tomllib module for TOML support.

Optional features can be installed individually or together:

pip install gway-sigils[dotenv]
pip install gway-sigils[yaml]
pip install gway-sigils[markdown]
pip install gway-sigils[astronomy]
pip install gway-sigils[all]

Installation

Install Sigils using pip:

pip install gway-sigils

Syntax

[...] is the canonical sigil syntax. Normal sigils are lazy: wrapping text in Sigil parses and preserves them, and they are resolved only when solve() or the % operator is used.

from sigils import Sigil

template = Sigil("Hello, [user.name]!")
context = {"user": {"name": "Alice"}}

print(template.solve(context))  # Hello, Alice!
print(template % context)       # Hello, Alice!

A leading % changes when a sigil is resolved. %[...] is eager and is resolved as soon as the text is placed inside a Sigil envelope.

In Python, eager sigils use the current execution context in this order:

  1. caller locals

  2. caller globals

  3. the active Context

  4. built-in Sigils tools

from sigils import Sigil

name = "Alice"
template = Sigil("Hello, %[name]!")

print(template.template)  # Hello, Alice!

Normal and eager sigils can coexist. This allows early-bound values from the execution environment and late-bound values supplied by a later caller:

from sigils import Sigil

project = "gway"
template = Sigil("Project: %[project], user: [user]")

print(template.template)
# Project: gway, user: [user]

print(template.solve({"user": "Alice"}))
# Project: gway, user: Alice

If an eager sigil cannot be resolved from the current execution context, it is preserved intact so that a later explicit solve can still resolve it:

from sigils import Sigil

template = Sigil("%[available_later]")
print(template.template)  # %[available_later]

print(template.solve({"available_later": "ready"}))
# ready

Context

Context contributes ambient values to eager sigils without requiring those values to be passed to every Sigil constructor.

from sigils import Context, Sigil

with Context({"greeting": "Hello, world!"}):
    template = Sigil("%[greeting]")

print(template.template)  # Hello, world!

Caller locals and globals take precedence over values from Context. Explicit solve(context) resolution uses the context passed to solve; it does not implicitly merge the ambient Context.

Nested values, lists, and tools

Dotted sigils traverse nested dictionaries, lists, and object attributes:

from sigils import Sigil

context = {
    "users": [
        {"name": "Alice"},
        {"name": "Bob"},
    ]
}

print(Sigil("[users.1.name]") % context)  # Bob

Dictionary keys are case-sensitive and exact. For example, [name] and [Name] may refer to two different values in the same context.

Built-in tools can be chained as part of an expression:

context = {"name": "alice"}

print(Sigil("[name.upper]") % context)  # ALICE

Functions in the context may also be called. Arguments are separated from the function name by spaces:

context = {
    "name": "Alice",
    "greet": lambda name: f"Hello, {name}!",
}

print(Sigil("[greet name]") % context)
# Hello, Alice!

Prefixing an argument with % treats that argument as a literal value instead of resolving it as another sigil:

print(Sigil("[greet %name]") % context)
# Hello, name!

Multi-argument tools receive all declared arguments after each argument is resolved. For example, structured-data tools can parse a value and select a field in one expression:

context = {
    "payload": '{"name": "Alice"}',
    "field": "name",
}

print(Sigil("[json payload field]") % context)
# Alice

The built-in sigil tool emits canonical lazy syntax, so applying it to name produces [name] rather than an eager token.

Recursive interpolation

Values may themselves contain sigils. Explicit resolution recursively resolves lazy and eager sigils up to the configured maximum depth.

During constructor-time eager resolution, only eager sigils are consumed. If an eager value expands to a lazy token, that lazy token remains available for a later explicit solve.

from sigils import Sigil

early = "[late]"
template = Sigil("%[early]")

print(template.template)  # [late]
print(template.solve({"late": "resolved later"}))
# resolved later

Command-Line Usage

The CLI resolves normal [...] sigils against values supplied on the command line or loaded from a JSON/TOML context file:

sigils "Hello, [user.name]!" -c context.json
sigils "Hello, [name]!" -v name=Alice
sigils -e name.upper -v name=alice

Use --max-depth to control recursive interpolation and --list-sep to choose the separator used when a dictionary is rendered as its keys:

sigils "[a]" -d 0 -v 'a=[b]' -v b=resolved
sigils "[mapping]" -c context.json --list-sep ","

Files can be rendered to standard output, written elsewhere, or overwritten:

sigils -f template.conf -c context.toml
sigils -f template.conf -w generated.conf -c context.toml
sigils -f template.conf -r -c context.toml

When -f points to a directory, files whose names contain sigils are resolved recursively. Both the generated filename and its file contents use the supplied context. A resolved filename must remain a basename in the same directory; absolute paths and parent-relative paths are rejected. Existing destinations are also rejected unless --overwrite is explicitly supplied. Symlinks are removed before an explicit overwrite so rendering never follows a pre-existing symlink outside the selected directory.

--value entries are merged only into mapping contexts. JSON list or scalar contexts remain usable when no --value merge is requested.

The CLI deliberately contains only interpolation operations. Historical benchmark, test-runner, make, and package-release switches are development concerns and are no longer exposed as public CLI flags.

Protected values

Secret marks a value as sensitive without changing sigil syntax. It is a redaction and taint-propagation primitive, not encryption.

from sigils import Secret, Sigil

password = Secret("swordfish")

print(password)       # [REDACTED]
print(repr(password)) # Secret('[REDACTED]')
print(password.reveal())  # swordfish -- explicit trusted access

template = Sigil("password=[password]")
context = {"password": password}

print(template.solve(context))
# password=swordfish

print(template.results(context))
# {'password': '[REDACTED]'}

Template rendering is an intentional output operation, so it can consume the real wrapped value. Resolver introspection through results() recursively redacts protected values instead. Dotted traversal, built-in tools, callable arguments, and recursive interpolation preserve the protection marker.

Eager protected values are captured out-of-band instead of storing their raw text in the template:

password = Secret("swordfish")
template = Sigil("password=%[password]")

print(template.template)  # password=[REDACTED]
print(template.solve())   # password=swordfish

Use reveal() only at an explicit trusted boundary. Secret does not stop code that deliberately unwraps or renders the value; its purpose is to prevent accidental disclosure through ordinary representations and introspection.

Considerations

  • Function Execution: If the value of a sigil is callable, it may be executed and its return value used in the string. Only provide contexts and tools that are safe to execute.

  • Recursion Depth: Sigils resolves recursively up to 6 levels by default. Pass max_depth to Sigil or --max-depth to the CLI to choose a different limit.

  • Thread Safety: Context uses thread-local state. Mutable objects stored inside a context still require normal application-level synchronization.

  • Eager Python Context: %[...] reads the Python caller’s locals and globals. Use lazy [...] when a template should depend only on an explicit context supplied later.

  • Environment Tool: the env built-in exposes only explicitly allowlisted names. Common non-secret process values such as PATH, HOME, USER, SHELL, LANG, PWD, TERM, and TZ are allowed by default. Embedding applications can expose additional names with the comma-separated SIGILS_ENV_ALLOWLIST environment variable. Unapproved names return an empty string, and requesting the complete environment returns only approved entries.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gway_sigils-0.4.2.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gway_sigils-0.4.2-py3-none-any.whl (17.7 kB view details)

Uploaded Python 3

File details

Details for the file gway_sigils-0.4.2.tar.gz.

File metadata

  • Download URL: gway_sigils-0.4.2.tar.gz
  • Upload date:
  • Size: 22.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gway_sigils-0.4.2.tar.gz
Algorithm Hash digest
SHA256 fb4b954bdc8d898a49ff57d94fc7365f1c9d70e678733877dd9a78671344ec1a
MD5 fcc171299284f2fec68900110aaba796
BLAKE2b-256 73f34e98b5c63e9d4573b4eafa7912232f20e9e1c97e9d3ffe90e7c900c43db3

See more details on using hashes here.

Provenance

The following attestation bundles were made for gway_sigils-0.4.2.tar.gz:

Publisher: release.yml on arthexis/sigils

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gway_sigils-0.4.2-py3-none-any.whl.

File metadata

  • Download URL: gway_sigils-0.4.2-py3-none-any.whl
  • Upload date:
  • Size: 17.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gway_sigils-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f28fe5c7761ea36a1372ca96333369e04f20bafbc567f474a44d232a0e679076
MD5 b0f131d1ae384745dcd102f68b2edcda
BLAKE2b-256 e225eae615e7cec2ceeccf3a6add4157a1169aa07f5a0ae26b0be07855445ef9

See more details on using hashes here.

Provenance

The following attestation bundles were made for gway_sigils-0.4.2-py3-none-any.whl:

Publisher: release.yml on arthexis/sigils

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.7

2 files

0.4.6

2 files

0.4.5

2 files

0.4.4

2 files

0.4.3

2 files

This release

0.4.2 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page