A strongly typed environment variable management library for Python
Project description
stenv 
A Python decorator for generating meaningfully type-safe environment variable accessors.
Currently, only pyright is capable of correctly type checking the generated accessors. pyre,
mypy, and pytype will all report false positives.
Requirements
- Python 3.10+
Installation
pip install stenv
Example
stenv provides a way to access environment variables with automatic type conversion based on
type annotations. The types are meaningful and can be checked by static type checkers: optionals
might be None, whereas non-optional types must be set (either in the environment or as a default
value).
from pathlib import Path
from stenv import env
class Env:
prefix = "MYAPP_"
@env[Path]("PATH", default="./config")
def config_path():
pass
@env[int | None]("PORT")
def port():
pass
# The following line returns a Path object read from MYAPP_PATH environment
# variable or the ./config default if not set.
print(Env.config_path)
# Since Env.port is an optional type, we need to check if it is not None,
# otherwise type checking will fail.
if Env.port is not None:
print(Env.port) #< We can expect Env.port to be an integer here.
Usage
Required Environment Variables
If a type is not optional, it must be set either in the environment or as a default value.
from stenv import env
class Env:
@env[str]("API_KEY")
def api_key():
pass
This class definition will raise a RuntimeError if the API_KEY environment variable is not set
when the class is imported.
Values can be defined optional (e.g. int | None, Optional[int], Union[int, None]) which
removes this enforcement while also informing the type checker that the value might be None:
from stenv import env
class Env:
@env[int | None]("PORT")
def port():
pass
It is also possible to define a default value that will be used if the environment variable is not set.
from stenv import env
class Env:
@env[str]("API_KEY", default="default_api_key")
def api_key():
pass
Environment Variable Prefixing
from stenv import env
from pathlib import Path
from typing import Optional
class AppConfig:
prefix = "APP_" # Will be prepended to all environment variable names.
@env[int]("PORT", default=8000)
def port(): #< Will be transformed into a class property with type int.
pass
@env[Path | None]("LOG_FILE")
def log_file(): #< Will be transformed into a class property with type Path | None.
pass
print(AppConfig.port) # APP_PORT environment variable
print(AppConfig.log_file) # APP_LOG_FILE environment variable
Custom types
It is possible to use a custom type with a constructor that takes a string.
import re
class Email:
def __init__(self, email_string: str):
if not re.match(r"[^@]+@[^@]+\.[^@]+", email_string):
raise ValueError(f"Invalid email: {email_string}")
self.address = email_string
self.username, self.domain = email_string.split("@", 1)
def __eq__(self, other):
if isinstance(other, Email):
return self.address == other.address
return False
class Env:
@env[Email]("EMAIL")
def email():
pass
print(Env.email.username)
print(Env.email.domain)
Parsers
Parser functions may be used to convert the environment variable value to a different type.
from datetime import date
def parse_numlist(s: str) -> list[int]:
return [int(x) for x in s.split(",")]
class Env:
@env[date]("TEST_DATE", parser=date.fromisoformat)
def test_date():
pass
@env[list[int]]("TEST_NUMBERS", parser=parse_numlist)
def numbers():
pass
print(Env.test_date)
print(Env.numbers)
Type annotations are optional
While type annotations are optional, leaving them out kinda defeats the purpose of the library,
for the most part. That said, when type annotations are not provided, the type will be assumed to
be str.
class Env:
@env("API_KEY")
def api_key(): #< str
pass
Return types also work, but with a caveat
class Env:
@env("PORT")
def port() -> int:
pass
@env("API_KEY")
def api_key() -> str | None:
pass
The above code will do what you would expect (Env.port is an integer, Env.api_key is a string or
None), but the type checker will complain about the return type not matching the type annotation
and type metaprogramming in Python is not yet powerful enough to express this.
FAQ
Why would you do this?
Static type checking is a powerful way to catch bugs early in the development process. stenv
allows expressing assumptions about the environment variables a program uses.
It was also just fun to make.
Is this production-ready?
I used a version of this code in production for a while. That being said, this is an early
implementation that is yet to be battle-tested. The fact that only pyright can correctly type
check the generated accessors might be a deal-breaker for some. Use at your own risk.
What's with the name?
st in the name stands for statically typed. Initially, I wanted to name it some clever word play
on "env" (like envy, envious, etc.) but the amount of people who had the exact same idea on PyPI is
staggering.
License
MIT NON-AI
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 stenv-0.1.0.tar.gz.
File metadata
- Download URL: stenv-0.1.0.tar.gz
- Upload date:
- Size: 6.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e8ace5744db73556484558f1a8f0ef1c5f8a897d7792b380f63779ecbfabf62b
|
|
| MD5 |
cd70025d8c8aa039c56c32096a60b222
|
|
| BLAKE2b-256 |
e980da6536b0ad567a05340936e36c6634c6e2d60f1e97c33851d7cd3e6a25ed
|
File details
Details for the file stenv-0.1.0-py3-none-any.whl.
File metadata
- Download URL: stenv-0.1.0-py3-none-any.whl
- Upload date:
- Size: 6.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25d9c24932b43837b433db969fcfe0823a8514690cddd99cf4113685d94bb8e9
|
|
| MD5 |
9084fb42d35121225457e4a1cf3a87fa
|
|
| BLAKE2b-256 |
ac2125147648b7efefde3bdb2ff3c5d03faca678c447ce2263d5b716ecde116d
|