Strictly typed environment variable parsing and validation with msgspec.
Project description
strictenv
strictenv is a fast, strictly typed environment variable loader built on top of msgspec.
It gives you explicit schemas, predictable coercion, and runtime validation with a small API.
Install
uv add strictenv
Quickstart
from __future__ import annotations
from typing import Annotated
from msgspec import Struct
from strictenv import BaseSettings, Field, TransformStruct, transform
class Database(TransformStruct):
host: str
port: int
@transform("host", mode="before")
def normalize_host(value: str) -> str:
return value.strip().lower()
class AppSettings(BaseSettings):
debug: bool
database: Database
tenant_id: Annotated[str, Field(alias="TENANT")]
model_config = {
"env_prefix": "APP_",
"case_sensitive": False,
"env_nested_delimiter": "__",
"env_file": ".env",
"strict_env_file": True,
}
settings = AppSettings.load()
AppSettings.write_env_example(".env.example")
Examples:
APP_DEBUG=true->debug: boolAPP_DATABASE={"host":"localhost","port":5432}->database: DatabaseAPP_DATABASE__HOST=localhost+APP_DATABASE__PORT=5432-> nested parsingAPP_TENANT=acme->tenant_idvia alias
model_config
| Key | Type | Default | Description |
|---|---|---|---|
env_prefix |
str |
"" |
Prefix applied to all environment keys. |
case_sensitive |
bool |
False |
When False, key lookup is case-insensitive. |
env_nested_delimiter |
str | None |
None |
Enables nested mapping like DB__HOST. |
env_file |
str | None |
None |
Path to a .env file to load first. |
strict_env_file |
bool |
True |
When True, invalid/missing .env files raise explicit errors. |
max_nested_struct_depth |
int | None |
None |
Maximum allowed depth for nested Struct traversal. |
Field(...)
Field works both in Annotated[...] and as a default value:
from typing import Annotated
from strictenv import BaseSettings, Field
class AppSettings(BaseSettings):
# Annotated metadata style
retries: Annotated[int, Field(gt=0, lt=10)]
# Default value style (alias + default + description)
tenant_id: str = Field("acme", alias="TENANT", description="Tenant identifier")
# Required when using `...`
token: str = Field(...)
Supported quick validations:
gt,ge,lt,lemin_length,max_length
Description source priority for metadata/examples:
Field(description=...)(highest priority)- attribute docstring right below the field
@transform(...) And TransformStruct
Use @transform(field_name, mode="before" | "after") on classes that inherit
from TransformStruct (including BaseSettings).
beforereceives raw string input and may return:- another
str(then normal coercion runs), or - a value already in target type.
- another
afterreceives already parsed value and must keep a compatible runtime type.
from strictenv import BaseSettings, TransformStruct, transform, transform_struct
class DatabaseConfig(TransformStruct):
host: str
port: int
@transform("host", mode="before")
def normalize_host(value: str) -> str:
return value.strip().lower()
@transform("port", mode="after")
def keep_int(value: int) -> int:
return value + 1
class AppSettings(BaseSettings):
database: DatabaseConfig
Rules:
field_namemust be top-level in that class (no dotted paths).- Multiple transforms run in definition order.
- Nested transforms apply only when nested type inherits
TransformStruct. - Nested settings can still use plain
msgspec.Struct; useTransformStructonly when you need@transform.
@transform_struct(...)
Use @transform_struct when you need to mutate the already-built struct instance.
from strictenv import BaseSettings, Field, transform_struct
class AppSettings(BaseSettings):
token: str = Field(..., min_length=4)
@transform_struct
def normalize(instance: AppSettings) -> None:
instance.token = instance.token.strip().lower()
Execution order:
beforefield transforms- parse/coerce
afterfield transformstransform_struct- final revalidation (runtime type compatibility + field constraints)
Notes:
transform_structapplies to anyTransformStruct(root and nested).- The hook must mutate in place and return
None. - Changing an attribute to an incompatible type raises
TransformSettingError.
Generate .env.example
BaseSettings.write_env_example(path) writes an empty env template for the schema.
Field descriptions are emitted as comments:
class AppSettings(BaseSettings):
debug: bool = Field(..., description="Enable debug logs")
tenant_id: str = Field(..., alias="TENANT", description="Tenant identifier")
AppSettings.write_env_example(".env.example")
Generated file:
# Enable debug logs
DEBUG=
# Tenant identifier
TENANT=
Value precedence
overridesargument inload(...)envargument (oros.environwhenenv=None).envfile configured withmodel_config["env_file"]- Field defaults in the settings struct
If no source provides a required field, MissingSettingError is raised.
If env_file is configured but missing, EnvFileNotFoundError is raised.
If env_file cannot be read, EnvFileReadError is raised.
If a non-comment line in env_file is not valid KEY=VALUE, EnvFileFormatError is raised.
If keys collide in case-insensitive mode, EnvKeyConflictError is raised.
If nested struct depth exceeds max_nested_struct_depth, NestedStructDepthError is raised.
With strict_env_file=False, .env file errors are tolerated and invalid lines are skipped.
Coercion rules
strictenv performs strict coercion for:
bool,int,float,strEnum(by member name or value)datetime,date,timetimedelta(ISO8601,HH:MM[:SS], or numeric seconds)msgspec.Struct(from JSON string)list,dict,tuple,set,Mapping(from JSON string)Union/Optional(tries non-Nonemembers in order)
Invalid values raise ParseSettingError. There is no silent fallback to raw strings.
Transform registration/execution failures raise TransformSettingError.
.env parser features:
- Optional
exportprefix (export KEY=value) - Inline comments for unquoted values (
KEY=value # comment) - Quoted values with escapes and multiline support
- Variable expansion via
${VAR}(including references to earlier/later keys)
Differences vs pydantic-settings
- API is intentionally smaller and focused on
msgspec.Struct. - Compatibility is partial (supports familiar
model_config, aliases, and nested env parsing). - Automatic field description injection into
msgspec.Metais supported.
Development
uv sync --dev
uv run ruff check .
uv run mypy src
uv run pytest
uv build
Contributing
See CONTRIBUTING.md for PR workflow, checks, and contribution guidelines.
Release (Maintainers)
Publishing is maintainer-only and handled by GitHub Actions on version tags.
Typical flow:
# 1) bump version in pyproject.toml and update CHANGELOG.md
git tag vX.Y.Z
git push origin vX.Y.Z
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file strictenv-0.1.2.tar.gz.
File metadata
- Download URL: strictenv-0.1.2.tar.gz
- Upload date:
- Size: 21.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a1b6bc944369bce7a6eb2f2724cafef04a7a773e8154101a06093ba9b7d408ba
|
|
| MD5 |
eedd7988a91760b5b3b34a51dc661528
|
|
| BLAKE2b-256 |
29f52dd34764780e42110b62d16433ae0ffa6393ce0e4a6e25f53200fa2820ad
|
File details
Details for the file strictenv-0.1.2-py3-none-any.whl.
File metadata
- Download URL: strictenv-0.1.2-py3-none-any.whl
- Upload date:
- Size: 25.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29b9d5776b74aae56c824c34c2ba24ade428910cc4f139bc1739241f11c6cc2c
|
|
| MD5 |
709471528558467f4ca27527f40a5055
|
|
| BLAKE2b-256 |
a744d6d6d0ae887b7ca9f512c49b3df268252d40d9c359ac8512c9e523b5c34c
|