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 four kinds: expose, var, export env, and import env — the last two integrate with the process environment (see below).

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

Environment integration

stec can push values out to, and pull values in from, the process environment.

export env NAME = value sets the OS environment variable NAME for the current process (child processes inherit it):

export env MYAPP_MODE = $dev ? "development" : "production"
  • Values are formatted like interpolation: true/false, 1.5, 8080.
  • Exported names join the document namespace, so later $name and ${name} references can use them.
  • Exported values are not published on the Stec singleton — use expose for that.

import env TYPE NAME = DEFAULT reads the environment variable NAME, coerces it to the declared type, and publishes it like expose. The default is optional:

import env int PORT = 8080        # falls back to 8080 when PORT is not set
import env string MYAPP_TOKEN     # fails at load time when MYAPP_TOKEN is not set
  • When the variable exists, its raw string is coerced to the type ("9090" becomes 9090); a value that cannot be coerced raises StecTypeError at load time.
  • bool accepts true / false, case-insensitively.
  • When the variable is missing, the default is evaluated and used; with no default, loading fails with StecNameError.

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 and import env declarations are published; var and export env 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 type, a ternary condition is not a bool, an environment value cannot be coerced, or an import env default has the wrong type.
StecNameError An undefined reference, a duplicate declaration, or a required environment variable that is not set.
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.2.1

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.2.1
File Size Uploaded
stec-0.2.1.tar.gz 24.3 kB Details

Built distribution (wheel)

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

Total release size: 44.0 kB

Release files / stec-0.2.1.tar.gz

Download URL stec-0.2.1.tar.gz
Size 24.3 kB
Tags Source
SHA-256 checksum
How to use checksums
01ec387543670f14cec98f92d2c1115b5bbe0172ef7e5c34c3cd41eb262c50a5
BLAKE2b-256 checksum
How to use checksums
0dace8766357088de367e539bb94ad726d083eae9298cae4460348fc7d7f936a
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.2.1-py3-none-any.whl

Download URL stec-0.2.1-py3-none-any.whl
Size 19.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bf0f0b48f686b49d5f52b46f0bafb51369cb31d4df2d56b9aa819caaaf24ee0f
BLAKE2b-256 checksum
How to use checksums
7ed23c91012f88a2b00ca1448df2b130ad604a59e54645819b9d8331b33f1d58
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

This release

0.2.1 This release

2 release files

0.1.0

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