🌅 Kankyō
Define your application's environment variables in a Python schema, then generate a typed configuration client.
Define, generate, load
Install into your application's virtual environment:
python -m pip install kankyo
Create env_schema.py. Keyword names become attributes on the generated client:
from kankyo import env, url, Schema, secret, boolean, integer, one_of
schema = Schema(
database_url=env('DATABASE_URL', url()),
port=env('PORT', integer(minimum=1, maximum=65535), default=8080),
debug=env('DEBUG', boolean(), optional=True),
log_level=env('LOG_LEVEL', one_of('debug', 'info', 'warning', 'error'), default='info'),
api_token=env('API_TOKEN', secret(min_length=1)),
)
Generate the client:
kankyo generate env_schema.py --output config.py
The equivalent module command is python -m kankyo generate env_schema.py --output config.py.
Use the client in your application:
from config import load
config = load(environment='production')
config.database_url # str
config.port # int
config.debug # bool | None
config.log_level # Literal['debug', 'info', 'warning', 'error']
config.api_token # Secret
token = config.api_token.reveal()
The generated module contains a frozen dataclass, concrete annotations, and load() / load_from() functions.
Editors and type checkers can inspect it directly. Commit it alongside your schema and regenerate after schema
changes. Its schema is embedded in the generated file, so your application needs Kankyo at runtime but does not
need to import or ship env_schema.py. Importing the client does not read environment files or process variables.
See the example schema and its generated client.
Loading priority
load() uses the current directory as its root. Pass root=... to select another directory. Each layer overrides
the preceding layers in this order:
| Priority | Source |
|---|---|
| 1 | .env |
| 2 | .env.<environment> |
| 3 | .env.local |
| 4 | .env.<environment>.local |
| 5 | os.environ |
| 6 | Explicit overrides |
config = load(root='settings', environment='production', overrides={'PORT': '9000'})
Environment-specific files are used only when you supply environment. Missing files are fine; malformed or
unreadable files raise SourceError, even if a later layer would override their values. Loading never changes
os.environ. Keep local environment files out of version control.
Files support UTF-8, an optional BOM, export NAME=value, empty values, comments, and single or double quotes.
Unquoted inline comments start with # after whitespace. Quoted values can span lines. Double quotes recognize
\n, \r, \t, \", and \\; single quotes preserve backslashes. Values are literal: ${NAME} is not expanded
and shell commands are never evaluated. Duplicate keys in a file use the last value.
Fields and defaults
Every field is required unless it has a default or optional=True:
from kankyo import env, Schema, string, boolean, integer
schema = Schema(
host=env('HOST', string(min_length=1)),
port=env('PORT', integer(minimum=1), default=8080),
debug=env('DEBUG', boolean(), optional=True),
region=env('REGION', string(), optional=True, default='us-east'),
)
An optional field without a default becomes T | None. A field with a default always has type T, including when
optional=True is set. Defaults accept Python values or environment strings and are parsed and validated when the
schema is defined. Mutable defaults are snapshotted and copied through parsing on each load. Use optional=True
for missing values; None is not a default value.
An empty string counts as present. It never activates a default or an alias. Add min_length=1 when a string must
be non-empty. Environment mappings must contain strings; Python values are accepted only for schema defaults.
Aliases let a field read from alternative environment variable names:
database_url=env('DATABASE_URL', url(), aliases=('DB_URL',))
The primary name wins when present, followed by aliases in their declared order. An invalid primary value is an
error. Use description='...' to include a field comment in the generated dataclass.
Rules
| Rule | Generated Python type | Options / behavior |
|---|---|---|
string() |
str |
strip, min_length, max_length, full-match pattern |
integer() |
int |
Base-10 integers; minimum, maximum |
number() |
float |
Finite numbers; minimum, maximum |
decimal() |
Decimal |
Exact finite decimals; minimum, maximum |
boolean() |
bool |
Case-insensitive true/false, yes/no, on/off, 1/0 |
one_of('dev', 'prod') |
Literal['dev', 'prod'] |
Exact, case-sensitive string choices |
duration() |
timedelta |
Numeric seconds or compound units: d, h, m, s, ms, us |
path() |
Path |
Non-empty paths; expands ~; no filesystem existence check |
url() |
str |
Absolute URL with a host and valid port; optional schemes=('https',) |
uuid() |
UUID |
UUID strings |
ip_address() |
`IPv4Address | IPv6Address` |
secret() |
Secret |
strip, min_length, max_length; redacted display |
list_of(rule) |
list[T] |
Typed CSV or JSON arrays; separator, min_length, max_length, unique |
String whitespace is preserved unless strip=True. Numeric and boolean strings allow surrounding whitespace.
Numeric bounds are inclusive. Duration fractions are truncated to Python's microsecond precision. Paths are
relative to the application's current directory, including when dotenv files come from another root.
Lists validate every item, including items in defaults. CSV supports quoted separators, and whitespace after a separator is skipped. Empty input produces an empty list. Nested lists use JSON arrays:
from kankyo import env, Schema, string, integer, list_of
schema = Schema(
hosts=env('HOSTS', list_of(string(min_length=1), unique=True), default=['localhost']),
ports=env('PORTS', list_of(integer(minimum=1)), default=[8080, 8081]),
groups=env('GROUPS', list_of(list_of(integer())), default=[[1, 2], [3]]),
)
Rules are immutable and can be reused across fields. Kankyo deliberately keeps the rule set focused on common configuration values. Application-specific transformations can run after loading.
Validation and secrets
load() and load_from() parse all fields before returning a client. They raise one ConfigError containing every
invalid or missing field. Unrelated environment variables are ignored.
from config import load
from kankyo import ConfigError
try:
config = load()
except ConfigError as error:
for issue in error.issues:
print(issue.field, issue.variable, issue.reason)
raise SystemExit(1)
Errors identify fields and rules without including input values. str(secret), repr(secret), and the generated
client's representation redact secrets. Call .reveal() to retrieve the actual string. Redaction is for display;
it is not encryption. Schema defaults are embedded in generated source, so real credentials belong in the
environment rather than defaults.
SchemaError covers invalid definitions, SourceError covers dotenv failures, and GenerationError covers
generation failures. All inherit from KankyoError.
Testing your application
load_from() reads only the supplied mapping. It does not read dotenv files or merge process variables:
from config import load_from
config = load_from({
'DATABASE_URL': 'postgres://localhost/app',
'API_TOKEN': 'test-token',
'PORT': '9000',
})
assert config.port == 9000
assert config.debug is None
The dataclass prevents attribute reassignment. List fields remain ordinary mutable Python lists, with independent values for each load. Constructing the dataclass directly does not run validation; use the generated loaders for environment input.
Generator options
kankyo generate env_schema.py --output app/config.py --class-name Settings
kankyo generate env_schema.py --name production --output app/config.py
kankyo generate env_schema.py --output app/config.py --check
Without --output, the client is written to config_gen.py beside the schema. A file must define exactly one public
Schema instance unless --name selects one. Schemas are normal Python files executed during generation, so use
files you trust. Put schema files in modules separate from their generated clients.
Generation is deterministic and writes atomically. --check exits with a failure when output is missing or stale
and never writes the output. Handwritten files require --force to replace. The schema file itself cannot be used
as the output. Don't format generated files separately, since that would cause freshness checks to fail.
For programmatic generation, kankyo.generate exposes load_schema(), render(), and generate().
Development
python -m venv .venv
# Activate .venv using your shell's activation command.
python -m pip install -e '.[dev]'
python -m mypy
python -m kankyo generate examples/env_schema.py --check
python -m pytest
python -m build
python -m twine check --strict dist/*
Tests runs on Linux, Windows, and macOS with Python 3.14. It checks types and generated output, builds
the wheel from the sdist, verifies package metadata, and tests the installed wheel outside the checkout with
coverage. Distribution and coverage artifacts are retained for 14 days.
Metadata
Release files for kankyo 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| kankyo-1.0.1.tar.gz | 27.3 kB | With provenance | Details for kankyo-1.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| kankyo-1.0.1-py3-none-any.whl | Python 3 | none | any | Details for kankyo-1.0.1-py3-none-any.whl |
Total release size: 46.0 kB
Release files / kankyo-1.0.1.tar.gz
| Download URL | kankyo-1.0.1.tar.gz |
|---|---|
| Size | 27.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
98a5ad4500db7e5ada376982a2bef4c1392f4867a999fd2cff4e353ddedb9667
|
|
BLAKE2b-256 checksum How to use checksums |
1548f91f2aaf0287dab65b455fcedc2bc5546c6f33287b25170b9cce13d47e4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / kankyo-1.0.1-py3-none-any.whl
| Download URL | kankyo-1.0.1-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8d6945d0e37e700d680a523030c3dfa1822787d647e2e95bd5b80bc479810d2a
|
|
BLAKE2b-256 checksum How to use checksums |
2cbeff25a92286e261a070868a359105ae28c36a3621b57250e24e8cc54ef077
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logSecurity for kankyo 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for kankyo-1.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.