Skip to main content

A strongly typed environment variable management library for Python

Project description

stenv Python 3.10+

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stenv-0.1.0.tar.gz (6.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stenv-0.1.0-py3-none-any.whl (6.8 kB view details)

Uploaded Python 3

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

Hashes for stenv-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e8ace5744db73556484558f1a8f0ef1c5f8a897d7792b380f63779ecbfabf62b
MD5 cd70025d8c8aa039c56c32096a60b222
BLAKE2b-256 e980da6536b0ad567a05340936e36c6634c6e2d60f1e97c33851d7cd3e6a25ed

See more details on using hashes here.

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

Hashes for stenv-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 25d9c24932b43837b433db969fcfe0823a8514690cddd99cf4113685d94bb8e9
MD5 9084fb42d35121225457e4a1cf3a87fa
BLAKE2b-256 ac2125147648b7efefde3bdb2ff3c5d03faca678c447ce2263d5b716ecde116d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page