Package: better-pyenv
better-pyenv: simplified environment variable parsingbetter-pyenvis a Python library for parsing environment variables.
It allows you to store configuration separate from your code, as perThe Twelve-Factor Appmethodology.ContentsFeaturesInstallBasic usageSupported typesReading .env filesReading a specific fileHandling prefixesVariable expansionValidationDeferred validationSerializationDefining custom parser behaviorUsage with FlaskUsage with DjangoWhy...?Why envvars?Why not os.environ?Why another library?LicenseFeaturesType-castingRead.envfiles intoos.environ(useful for local development)ValidationDefine custom parser behaviorFramework-agnostic, but integrates well withFlaskandDjangoInstallpip install better-pyenvBasic usageWith some environment variables set...exportGITHUB_USER=sloriaexportMAX_CONNECTIONS=100exportSHIP_DATE='1984-06-25'exportTTL=42exportENABLE_LOGIN=trueexportGITHUB_REPOS=webargs,konch,pedexportGITHUB_REPO_PRIORITY="webargs=2,konch=3"exportCOORDINATES=23.3,50.0exportLOG_LEVEL=DEBUGParse them with better-pyenv...frombetter_pyenvimportEnvenv=Env()env.read_env()# read .env file, if it exists# required variablesgh_user=env("GITHUB_USER")# => 'sloria'secret=env("SECRET")# => raises error if not set# castingmax_connections=env.int("MAX_CONNECTIONS")# => 100ship_date=env.date("SHIP_DATE")# => datetime.date(1984, 6, 25)ttl=env.timedelta("TTL")# => datetime.timedelta(0, 42)log_level=env.log_level("LOG_LEVEL")# => logging.DEBUG# providing a default valueenable_login=env.bool("ENABLE_LOGIN",False)# => Trueenable_feature_x=env.bool("ENABLE_FEATURE_X",False)# => False# parsing listsgh_repos=env.list("GITHUB_REPOS")# => ['webargs', 'konch', 'ped']coords=env.list("COORDINATES",subcast=float)# => [23.3, 50.0]# parsing dictsgh_repos_priorities=env.dict("GITHUB_REPO_PRIORITY",subcast_values=int)# => {'webargs': 2, 'konch': 3}Supported typesThe following are all type-casting methods ofEnv:env.strenv.boolenv.intenv.floatenv.decimalenv.list(accepts optionalsubcastanddelimiterkeyword arguments)env.dict(accepts optionalsubcast_keysandsubcast_valueskeyword arguments)env.jsonenv.datetimeenv.dateenv.timeenv.timedelta(assumes value is an integer in seconds)env.urlenv.uuidenv.log_levelenv.path(casts to apathlib.Path)env.enum(casts to any given enum type specified intypekeyword argument, accepts optionalignore_casekeyword argument)Reading.envfiles# .envDEBUG=truePORT=4567CallEnv.read_envbefore parsing variables.frombetter_pyenvimportEnvenv=Env()# Read .env into os.environenv.read_env()env.bool("DEBUG")# => Trueenv.int("PORT")# => 4567Reading a specific fileBy default,Env.read_envwill look for a.envfile in current
directory and (if no .env exists in the CWD) recurse
upwards until a.envfile is found.You can also read a specific file:frombetter_pyenvimportEnvwithopen(".env.test","w")asfobj:fobj.write("A=foo\n")fobj.write("B=123\n")env=Env()env.read_env(".env.test",recurse=False)assertenv("A")=="foo"assertenv.int("B")==123Handling prefixes# export MYAPP_HOST=lolcathost# export MYAPP_PORT=3000withenv.prefixed("MYAPP_"):host=env("HOST","localhost")# => 'lolcathost'port=env.int("PORT",5000)# => 3000# nested prefixes are also supported:# export MYAPP_DB_HOST=lolcathost# export MYAPP_DB_PORT=10101withenv.prefixed("MYAPP_"):withenv.prefixed("DB_"):db_host=env("HOST","lolcathost")db_port=env.int("PORT",10101)Variable expansion# export CONNECTION_URL=https://${USER:-sloria}:${PASSWORD}@${HOST:-localhost}/# export PASSWORD=secret# export YEAR=${CURRENT_YEAR:-2020}frombetter_pyenvimportEnvenv=Env(expand_vars=True)connection_url=env("CONNECTION_URL")# =>'https://sloria:secret@localhost'year=env.int("YEAR")# =>2020Validation# export TTL=-2# export NODE_ENV='invalid'# export EMAIL='^_^'frombetter_pyenvimportEnvfrommarshmallow.validateimportOneOf,Length,Emailenv=Env()# simple validatorenv.int("TTL",validate=lambdan:n>0)# => Environment variable "TTL" invalid: ['Invalid value.']# using marshmallow validatorsenv.str("NODE_ENV",validate=OneOf(["production","development"],error="NODE_ENV must be one of:{choices}"),)# => Environment variable "NODE_ENV" invalid: ['NODE_ENV must be one of: production, development']# multiple validatorsenv.str("EMAIL",validate=[Length(min=4),Email()])# => Environment variable "EMAIL" invalid: ['Shorter than minimum length 4.', 'Not a valid email address.']Deferred validationBy default, a validation error is raised immediately upon calling a parser method for an invalid environment variable.
To defer validation and raise an exception with the combined error messages for all invalid variables, passeager=FalsetoEnv.
Callenv.seal()after all variables have been parsed.# export TTL=-2# export NODE_ENV='invalid'# export EMAIL='^_^'frombetter_pyenvimportEnvfrommarshmallow.validateimportOneOf,Email,Length,Rangeenv=Env(eager=False)TTL=env.int("TTL",validate=Range(min=0,max=100))NODE_ENV=env.str("NODE_ENV",validate=OneOf(["production","development"],error="NODE_ENV must be one of:{choices}"),)EMAIL=env.str("EMAIL",validate=[Length(min=4),Email()])env.seal()# better_pyenv.EnvValidationError: Environment variables invalid: {'TTL': ['Must be greater than or equal to 0 and less than or equal to 100.'], 'NODE_ENV': ['NODE_ENV must be one of: production, development'], 'EMAIL': ['Shorter than minimum length 4.', 'Not a valid email address.']}env.seal()validates all parsed variables and prevents further parsing (calling a parser method will raise an error).Serialization# serialize to a dictionary of simple types (numbers and strings)env.dump()# {'COORDINATES': [23.3, 50.0],# 'ENABLE_FEATURE_X': False,# 'ENABLE_LOGIN': True,# 'GITHUB_REPOS': ['webargs', 'konch', 'ped'],# 'GITHUB_USER': 'sloria',# 'MAX_CONNECTIONS': 100,# 'MYAPP_HOST': 'lolcathost',# 'MYAPP_PORT': 3000,# 'SHIP_DATE': '1984-06-25',# 'TTL': 42}Defining custom parser behavior# export DOMAIN='http://myapp.com'# export COLOR=invalidfromfurlimportfurl# Register a new parser method for paths@env.parser_for("furl")deffurl_parser(value):returnfurl(value)domain=env.furl("DOMAIN")# => furl('https://myapp.com')# Custom parsers can take extra keyword arguments@env.parser_for("choice")defchoice_parser(value,choices):ifvaluenotinchoices:raisebetter_pyenv.EnvError("Invalid!")returnvaluecolor=env.choice("COLOR",choices=["black"])# => raises EnvErrorUsage with Flask# myapp/settings.pyfrombetter_pyenvimportEnvenv=Env()env.read_env()# Override in .env for local developmentDEBUG=env.bool("FLASK_DEBUG",default=False)# SECRET_KEY is requiredSECRET_KEY=env.str("SECRET_KEY")Load the configuration after you initialize your app.# myapp/app.pyfromflaskimportFlaskapp=Flask(__name__)app.config.from_object("myapp.settings")For local development, use a.envfile to override the default
configuration.# .envDEBUG=trueSECRET_KEY="not so secret"Note: Because better-pyenv depends onpython-dotenv,
theflaskCLI will automatically read .env and .flaskenv files.Usage with Djangobetter-pyenv includes a number of helpers for parsing connection URLs. To
install better-pyenv with django support:pip install better-pyenv[django]Useenv.dj_db_url,env.dj_cache_urlandenv.dj_email_urlto parse theDATABASE_URL,CACHE_URLandEMAIL_URLenvironment variables, respectively.For more details on URL patterns, see the following projects that better-pyenv is using for converting URLs.dj-database-urldjango-cache-urldj-email-urlBasic example:# myproject/settings.pyfrombetter_pyenvimportEnvenv=Env()env.read_env()# Override in .env for local developmentDEBUG=env.bool("DEBUG",default=False)# SECRET_KEY is requiredSECRET_KEY=env.str("SECRET_KEY")# Parse database URLs, e.g.  "postgres://localhost:5432/mydb"DATABASES={"default":env.dj_db_url("DATABASE_URL")}# Parse email URLs, e.g. "smtp://"email=env.dj_email_url("EMAIL_URL",default="smtp://")EMAIL_HOST=email["EMAIL_HOST"]EMAIL_PORT=email["EMAIL_PORT"]EMAIL_HOST_PASSWORD=email["EMAIL_HOST_PASSWORD"]EMAIL_HOST_USER=email["EMAIL_HOST_USER"]EMAIL_USE_TLS=email["EMAIL_USE_TLS"]# Parse cache URLS, e.g "redis://localhost:6379/0"CACHES={"default":env.dj_cache_url("CACHE_URL")}For local development, use a.envfile to override the default
configuration.# .envDEBUG=trueSECRET_KEY="not so secret"Why...?Why envvars?SeeThe 12-factor Appsection onconfiguration.Why notos.environ?Whileos.environis enough for simple use cases, a typical application
will need a way to manipulate and validate raw environment variables.
better-pyenv abstracts common tasks for handling environment variables.better-pyenv will help youcast envvars to the correct typespecify required envvarsdefine default valuesvalidate envvarsparse list and dict valuesparse dates, datetimes, and timedeltasparse expanded variablesserialize your configuration to JSON, YAML, etc.Why another library?There are many great Python libraries for parsing environment variables.
In fact, most of the credit for better-pyenv' public API goes to the
authors ofenvparseanddjango-environ.better-pyenv aims to meet three additional goals:Make it easy to extend parsing behavior and develop plugins.Leverage the deserialization and validation functionality provided
by a separate library (marshmallow).Clean up redundant API.Seethis GitHub
issuewhich details specific differences with envparse.