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—trueorfalse
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.
$namemust 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
exposevalues. ${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=TrueraisesStecAlreadyLoadedError— 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
exposedeclarations are published;varvalues 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_PORTbeatsport)
License
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)
| File | Size | Uploaded | |
|---|---|---|---|
| stec-0.1.0.tar.gz | 21.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|