Skip to main content

stec

Simple, Typed, Expressive Config — a tiny, human-friendly configuration language for Python applications, with zero dependencies.

var dev = true

expose int port = 8080
expose string secret = "my-secret"
expose string url = $dev ? "127.0.0.1" : "0.0.0.0"
from stec import Stec

Stec.load("app.stec")

Stec.port   # 8080
Stec.url    # "127.0.0.1"

No YAML indentation traps, no .env string coercion — values are typed and validated when the file is loaded, so bad configuration fails fast at startup with a clear message and a line/column position.


Why stec?

Problem with .env / plain dicts How stec helps
Everything is a string (PORT="8080" is a string) Real types: int, float, string, bool
Nothing is validated Type errors raised at load time, with position
Repeated values everywhere $variables declared once, reused everywhere
Env-dependent values need code Built-in ternary: $dev ? "127.0.0.1" : "0.0.0.0"
Values scattered across files One file, loaded once, readable from everywhere

And it is just Python — no services, no DSL runtime, no dependencies.


Installation

Requires Python 3.10+. There are no runtime dependencies.

pip install stec

The language

A stec file is a list of declarations. There are two kinds:

expose — public, typed values

expose int port = 8080
expose float ratio = 1.5
expose string name = "my-app"
expose bool debug = false

An expose declares a value that is published by the Stec singleton. The declared type must match the value:

  • int — integer literals (8080, 0)
  • float — decimal literals (1.5, 0.25)
  • string — double-quoted strings ("hello")
  • bool — true or false

Typing is strict: bool is not accepted where int is expected, and int is not accepted where float is expected. This keeps errors obvious:

expose int port = "8080"   # Error: cannot expose string value '8080' as 'int'
expose float ratio = 1     # Error: cannot expose int value 1 as 'float'

var — internal variables

var dev = true

A var is not published. It exists to be referenced by $name in later declarations — ideal for environment switches or derived defaults.

$variables and ternaries

Values can reference previously declared names with $name, and any value can be made conditional with a ternary:

var dev = true

expose string url = $dev ? "127.0.0.1" : "0.0.0.0"
expose string name = $dev ? "my-app-dev" : "my-app"

Rules:

  • The condition must be a bool.
  • Ternaries are lazy: only the branch that is taken is evaluated.
  • $name must be declared before it is used.
  • Variables can be chained: cond ? $a ? 1 : 2 : 3.

Interpolation

Strings can embed previously declared names with ${name} — the value is rendered into the text:

var host = "localhost"

expose int port = 8080
expose string url = "http://${host}:${port}/"
expose string mode = "debug=${debug}"   # bools render as true/false
  • Any name declared before the string can be interpolated, including other expose values.
  • ${name} must be declared before the string that uses it.
  • Bools render as true/false, numbers render as written (1.5).
  • To include a literal ${...} in a string, escape the dollar: "\${price}".

Comments

# starts a comment that runs to the end of the line — it can occupy its own line or trail a declaration:

# runtime switches
var dev = true  # flip for prod

expose int port = 8080

Strings

Strings are double-quoted and support common escape sequences:

expose string greeting = "line\nbreak"
expose string quoted  = "she said \"hi\""
expose string unicode = "café \u00e9"
Escape Meaning
\\ backslash
\" double quote
\n newline
\t tab
\r carriage return
\0 null character
\b backspace
\f form feed
\$ dollar sign (prevents ${...} interpolation)
\uXXXX unicode code point (4 hex digits)

Names

Names must start with a letter or _ and may contain letters, digits, and _. $ followed by a name references a variable. Whitespace (including newlines) between tokens is ignored.


Using the singleton

Stec is a singleton: every module in your application that imports it gets the same object. Load once (typically at startup), read anywhere.

app.stec

var dev = true

expose int port = 8080
expose string secret = "my-secret"
expose string url = $dev ? "127.0.0.1" : "0.0.0.0"

main.py

from stec import Stec

Stec.load("app.stec")

Stec.port            # 8080          (attribute access)
Stec["url"]          # "127.0.0.1"   (item access)
Stec.get("port")     # 8080          (with optional default)
Stec.get("missing")  # None

API reference

Member Description
Stec.load(path, force=False) Parse and evaluate the file into the singleton. Returns the singleton itself (chainable).
Stec.<name> Attribute access to an exposed value.
Stec["<name>"] Item access to an exposed value.
Stec.get(name, default=None) Access with a fallback for missing values.
Stec.as_dict() Copy of all exposed values as a plain dict.
"name" in Stec Check whether a value is exposed.
Stec.is_loaded True once a file has been loaded.
Stec.loaded_from Path the configuration was loaded from, or None.
Stec.load(path, force=True) Reload, replacing all previous values.
Stec.reset() Forget the loaded configuration (useful in tests).

Notes:

  • Loading twice without force=True raises StecAlreadyLoadedError — silent double-loading usually hides a bug, so it is treated as one.
  • A failed load leaves the singleton unloaded: fix the file and load again.
  • Only expose declarations are published; var values stay internal.
  • repr(Stec) shows the loaded path and key names, but never values — safe to log even if the configuration contains secrets.
Stec.load("app.stec")
print(Stec)   # Stec(loaded_from='app.stec', keys=['port', 'secret', 'url'])

Error handling

All errors derive from StecError, so catching that single type is enough for a top-level handler. Errors raised while parsing or evaluating include a line/column position, and their messages display it.

from stec import Stec, StecError

try:
    Stec.load("app.stec")
except StecError as error:
    print(error)  # e.g. "cannot expose string value 'abc' as 'int' for 'port' at line 4, column 1"
Exception Raised when
StecError Base class for everything stec raises.
StecSyntaxError The file cannot be tokenized or parsed.
StecTypeError A value does not match the declared expose type, or a ternary condition is not a bool.
StecNameError A $variable is undefined or a name is declared twice.
StecLoadError The file cannot be read or decoded (missing file, bad UTF-8...).
StecAlreadyLoadedError load() is called twice without force=True.
expose int port = 8080
expose int port = 9090
StecNameError: duplicate declaration of 'port' at line 2, column 1

Quick example

app.stec

# runtime switches
var dev = true  # flip for prod

expose int port = 8080
expose int pool = 10
expose bool debug = $dev
expose string database_url = $dev
    ? "postgres://localhost:5432/app?pool=${pool}"
    : "postgres://db.internal:5432/app?pool=${pool}"

main.py

from stec import Stec

Stec.load("app.stec")

for key, value in Stec.as_dict().items():
    print(f"{key} = {value!r}")
port = 8080
pool = 10
debug = True
database_url = 'postgres://localhost:5432/app?pool=10'

Development

git clone https://github.com/<you>/stec.git
cd stec
pip install -e ".[dev]"
pytest

Project layout

src/stec/
├── nodes.py       # AST node dataclasses (Position, Literal, Ternary, ...)
├── parser.py      # hand-written tokenizer + recursive-descent parser
├── evaluator.py   # ordered evaluation and type checking
├── config.py      # the Stec singleton
├── errors.py      # exception hierarchy
└── main.py        # small playground (python src/stec/main.py)

tests/             # pytest suite (parser, evaluator, singleton)

The parser is written from scratch (no Lark, no YAML, nothing) so the package ships with zero runtime dependencies.


Roadmap

  • Comments (# to end of line)
  • String interpolation: "postgres://localhost:${port}"
  • Environment overrides (STEC_PORT beats port)

License

MPL-2.0

Metadata

Release files for stec 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stec 0.1.0
File Size Uploaded
stec-0.1.0.tar.gz 21.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stec 0.1.0
File Interpreter ABI Platform
stec-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 39.3 kB

Release files / stec-0.1.0.tar.gz

Download URL stec-0.1.0.tar.gz
Size 21.0 kB
Tags Source
SHA-256 checksum
How to use checksums
1c77be2c2d16e32b54ee0461294eb56449760f9a0de330bf8ce668ea1c83fb24
BLAKE2b-256 checksum
How to use checksums
3fae6431c8e8ac53dbe7c2abdb9501408e31a2d5afc1e538d607ee8d5b8ede62
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / stec-0.1.0-py3-none-any.whl

Download URL stec-0.1.0-py3-none-any.whl
Size 18.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
755d6a90897af5a29a6849f107db4ecefbe1c88a724c76b8df2adfd13a802e43
BLAKE2b-256 checksum
How to use checksums
53eb99979353607936bea3dfab58a5715698ebba9674e8eb39a94e7aed24bcb8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.1.0 This release

2 release 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