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.3.tar.gz (23.1 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.3-py3-none-any.whl (17.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: gway_sigils-0.4.3.tar.gz
  • Upload date:
  • Size: 23.1 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.3.tar.gz
Algorithm Hash digest
SHA256 e9aae5466fe9ed4a40f11143bd3c8e9ac84300987c20d1334aac4de17e27745d
MD5 fd7c3b1afecac6a43030567ef61d1427
BLAKE2b-256 721116fda9569b2f521de9ddb886fdab1980cc28e73f657a389395b78c925c8e

See more details on using hashes here.

Provenance

The following attestation bundles were made for gway_sigils-0.4.3.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.3-py3-none-any.whl.

File metadata

  • Download URL: gway_sigils-0.4.3-py3-none-any.whl
  • Upload date:
  • Size: 17.8 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.3-py3-none-any.whl
Algorithm Hash digest
SHA256 c3b2bea9705e96c2919eef2675c9ec05fac3f22b8aea5c2d12bcd7bcd47620eb
MD5 dcccb72981563ee37c473d544e1ff9ae
BLAKE2b-256 d044827dbc7851fdac4862ada2d60e5718367704d671343e2fe3b9c6b7bcb066

See more details on using hashes here.

Provenance

The following attestation bundles were made for gway_sigils-0.4.3-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

This release

0.4.3 This release

2 files

0.4.2

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