Skip to main content

EZ-Environment

“For easy access, baby!  …That’s right.’”—Eazy-E

Have you ever thought handling environment variables in Python should be easier? It is now.

TL;DR:

>>> import env

>>> env.SERVER_PORT.int
8080

“BAM!” —Emeril Lagasse

The E.Z.E.nvironment module also has its own theme song:

“We want Eazy!”

EAZY!
Everybody come on!
EAZY!
Who yall came to see?
EAZY!
A little louder come on!
EAZY!
Get those hands in the air!
EAZY!
Come on, come on say it!
EAZY!
A little louder come on!
EAZY!
Come on make some noise!

A miracle of modern creation…
EZ E’s on the set, hyped up with the bass
And a little bit of what ya love
From a brother who’s smooth like a criminal
I mean subliminal…

Background

It’s always been a tad clumsy to access environment variables and combine them with other strings in Python—compared to shell languages at least. For example, look how easy it is in (ba)sh:

⏵ echo "Libraries: $PWD/lib"
Libraries: /usr/local/lib

Unfortunately over in Python-land, required, escaped quotes and brackets serve mostly to complicate and add to visual clutter, reducing speed of comprehension.

>>> from os import environ
>>> from os.path import join

>>> join(environ['PWD'], 'lib')
'/usr/local/lib'

Even the new-fangled string interpolation doesn’t help as much as might be expected:

>>> print(f'Libraries: {environ["PWD"]}/lib')
Libraries: /usr/local/lib

With that in mind, allow me to introduce the env module. With it I’ve tried to whittle complexity down, primarily through direct attribute access:

>>> import env

>>> print('Term:', env.TERM)
Term: xterm-256color

>>> print(f'Libraries: {env.PWD}/lib')
Libraries: /usr/local/lib

But wait, there’s more!

Install

⏵ pip3 install --user ezenv  # env was taken :-/

 ☛ LGPL licensed. ☚

Environment and options

On import the module loads the environment into its namespace, thereby working like a dictionary with convenient attribute access.

So, no additional mapping instance has to be created or imported, unless you’d like to configure the interface further. The following options are available to customize:

>>> from env import Environment

>>> env = Environment(
        environ=os.environ,
        sensitive=True|False,  # case: platform default
        writable=False,
        pathsep=os.pathsep,
    )

Param: environ

A mapping of your own choosing may optionally be passed in as the first argument, for testing and/or other purposes. I’ve recently learned that os.environb (bytes interface) is a thing that could be passed, for example.

Param: writable

By default the Environment object/module does not allow modification since writing is rarely needed. This default helps to remind us of that fact, though the object can be easily be changed to writable if need be by enabling this option.

Param: sensitivity 😢

Variables are case-sensitive by default on Unix, insensitive under Windows.

While case sensitivity can be disabled to use variable names in mixed or lower-case, be aware that variables and dictionary methods are in the same namespace, which could potentially be problematic if they are not divided by case. For this reason, using variable names such as “keys” and “items” are not a good idea while in insensitive mode. shrug

Workaround: use “get item” / dictionary-style syntax if needed:

env['keys']  # :-/

Entry Objects

While using env at the interactive prompt, you may be surprised that a variable value is not a simple string but rather an extended string-like object called an “Entry.” This is most evident at the prompt since it prints a “representation” form by default:

>>> env.PWD                         # a.k.a. repr()
Entry('PWD', '/usr/local')

The reason behind this custom object is so that the variables can offer additional functionality, such as parsing or conversion of the value to another type, while not crashing on a non-existent attribute access.

No matter however, as we’ve seen in the previous sections, just about any operation renders the string value as normal. Attributes .name and .value are also available for belt & suspenders types:

>>> print(env.PWD)
/usr/local

>>> env.PWD.name, env.PWD.value, str(env.PWD)
('PWD', '/tmp', '/tmp')

Remember the env object/module is also a standard dictionary, while entry values are also strings, so full Python functionality is available:

>>> for key, value in env.items():  # it's a dict*
        print(key, value)

# USER fred…

>>> env.USER.title()                # it's a str*
'Fred'

>>> env.TERM.partition('-')         # tip: a safer split
('xterm', '-', '256color')

*  Sung to the tune, “It’s a Sin,” by the Pet Shop Boys.

Conversions & Parsing

Another handy feature of Entry objects is convenient type conversion and parsing of values from strings. Additional properties for this functionality are available. For example:

>>> env.PI.float
3.1416

>>> env.STATUS.int
5150

>>> env.DATA.from_json
{'one': 1, 'two': 2, 'three': 3}

Truthy Values

Variable entries may contain boolean-like string values, such as 0, 1, yes, no, true, false, etc. To interpret them in a case-insensitive manner use the .truthy property:

>>> env.QT_ACCESSIBILITY
Entry('QT_ACCESSIBILITY', '1')

>>> env.QT_ACCESSIBILITY.truthy
True

>>> env = Environment(writable=True)
>>> env.QT_ACCESSIBILITY = '0'          # set to '0'

>>> env.QT_ACCESSIBILITY.truthy
False

Standard Boolean Tests

As always, standard tests or bool() on the entry can be done to check a string. Remember, such a test checks merely if the string is empty or not, and would also return True on '0' or 'false'.

Paths

Environment vars often contain a list of filesystem paths. To split such path strings on os.pathsep🔗, with optional conversion to pathlib.Path🔗² objects, use one or more of the following:

>>> env.XDG_DATA_DIRS.list
['/usr/local/share', '/usr/share', ...]  # strings

>>> env.SSH_AUTH_SOCK.path
Path('/run/user/1000/keyring/ssh')

>>> env.XDG_DATA_DIRS.path_list
[Path('/usr/local/share'), Path('/usr/share'), ...]

To split on a different character, simply do the split/partition on the string manually.

Examples

There are generally three cases for environment variables:

Variable exists, has value:

>>> env.USER                            # exists, repr
Entry('USER', 'fred')

>>> env.USER + '_suffix'                # str ops
'fred_suffix'

>>> env.USER.title()                    # str ops II
'Fred'

>>> print(f'term: {env.TERM}')          # via interpolation
term: xterm-256color

>>> bool(env.USER)                      # check exists & not empty
True

>>> key_name = 'PI'
>>> env[key_name]                       # getitem syntax
'3.1416'

>>> env.PI.float                        # type conversion
3.1416

>>> env.PORT.int or 9000                # type conv. w/ default
5150

>>> env.QT_ACCESSIBILITY.truthy         # 0/1/yes/no/true/false
True

>>> env.JSON_DATA.from_json.keys()
['one', 'three', 'two']

>>> env.XDG_DATA_DIRS.list
['/usr/local/share', '/usr/share']

Variable exists, but is blank:

>>> 'EMPTY' in env                      # check existence
True

>>> env.EMPTY                           # exists but empty
Entry('EMPTY', '')

>>> bool(env.EMPTY)                     # check exists & not empty
False

>>> env.EMPTY or 'default'              # exists, blank w/ default
'default'

Variable doesn’t exist:

>>> 'NO_EXISTO' in env                  # check existence
False

>>> env.NO_EXISTO or 'default'          # DNE with default
'default'

>>> env.NO_EXISTO                       # Doesn't exist repr
NullEntry('NO_EXISTO')

>>> bool(env.NO_EXISTO)                 # check exists & not empty
False

>>> env.XDG_DATA_DIRz.list              # DNE fallback
[]

for data_dir in env.XDG_DATA_DIR.list:
    # Don't need to worry if this exists or not,
    # if not, it will be skipped.
    pass

Compatibility

“What’s the frequency Kenneth?”

This module attempts compatibility with KR’s existing env package by implementing its prefix and map functions:

>>> env.prefix('XDG_')  # from_prefix preferred
{'config_dirs': '/etc/xdg/xdg-mate:/etc/xdg', ...}

>>> env.map(username='USER')
{'username': 'fred'}

The lowercase transform can be disabled by passing another false-like value as the second argument to prefix().

While the package above has the coveted env namespace on PyPI, ezenv uses the same simple module name and provides an implementation of the interface.

Tests

Can be run here:

⏵ python3 -m env -v

Though this module works under Python2, several of the tests don’t, because Py2 does Unicode differently or doesn’t have the facilities available to handle them by default (pathlib/f-string). Haven’t had the urge to work around that due to declining interest.

FYI, a reference to the original module object is kept at env._module just in case it is needed for some reason.

Testing with ezenv

When you’ve used ezenv in your project, it is easy to create a custom environment to operate under:

from env import Environment

def test_foo():
    import mymodule

    mymodule.env = Environment(environ=dict(NO_COLOR='1'))
    assert mymodule.color_is_disabled() == True

Pricing

“I’d buy THAT for a dollar!” :-D

Metadata

Release files for ezenv 0.92

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ezenv 0.92
File Size Uploaded Provenance Actions
ezenv-0.92.tar.gz 14.2 kB Without provenance Details for ezenv-0.92.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ezenv 0.92
File Interpreter ABI Platform Actions
ezenv-0.92-py2.py3-none-any.whl Python 2, Python 3 none any Details for ezenv-0.92-py2.py3-none-any.whl

Total release size: 25.1 kB

Release files / ezenv-0.92.tar.gz

Download URL ezenv-0.92.tar.gz
Size 14.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a5a5d001a2352517b5992322c1795890dd3c9cf0ac6ad7912b0768fe84f33173
BLAKE2b-256 checksum
How to use checksums
822f781f71802c462cdab8fcb4c826d18a2e4a410ab302d164a39b5e977dda10
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Python-urllib/3.8

Release files / ezenv-0.92-py2.py3-none-any.whl

Download URL ezenv-0.92-py2.py3-none-any.whl
Size 10.9 kB
Tags Python 2 Python 3
SHA-256 checksum
How to use checksums
fe4b20071c7497071d33530040f52f12a00dc938e5b5f7d5c4b7f922196c4ac6
BLAKE2b-256 checksum
How to use checksums
bc6e253ac7fee0cfd7db2ea5a51577dabaaeefcf3a71b9a8ac2fe060c84e5f84
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Python-urllib/3.8

Security for ezenv 0.92

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page