Skip to main content

envname

Lianes8 Certified

Validate and sanitize environment variable names against POSIX, shell and OS rules.

A program that creates environment variables from user input, config keys or .env files has to decide which names are valid. The answer depends on who reads the name: a shell only expands [A-Za-z_][A-Za-z0-9_]*, the POSIX utilities use upper-case names only, and the operating system stores almost anything. envname names those three rule sets, tells you why a name fails and where, and turns arbitrary text into a safe name.

Install

pip install envname

Python 3.10 or newer, no dependencies.

Usage

Validate

from envname import is_valid_env_name

print(is_valid_env_name("PATH"))  # True
print(is_valid_env_name("1ST_VALUE"))  # False
print(is_valid_env_name("MY-VAR"))  # False
print(is_valid_env_name("KEY=VALUE"))  # False
print(is_valid_env_name("path", rules="portable"))  # False
print(is_valid_env_name("my-var", rules="environ"))  # True

Explain what is wrong

check_env_name returns the name when it is valid and raises InvalidEnvName, a ValueError, when it is not. The error carries name, rules, a human reason and the index of the first offending character (None for an empty name).

from envname import InvalidEnvName, check_env_name

try:
    check_env_name("MY-VAR")
except InvalidEnvName as exc:
    print(exc)  # invalid shell environment variable name 'MY-VAR': '-' is not allowed at index 2
    print(exc.index)  # 2

print(check_env_name("DATABASE_URL", rules="portable"))  # DATABASE_URL

Sanitize

sanitize_env_name turns any text into a valid name. Accented letters, ligatures and full-width forms are folded to ASCII, every other run of characters outside [A-Za-z0-9_] becomes one underscore, an optional prefix (itself a valid name) goes in front, an underscore is added when the result would be empty or start with a digit, and the name is upper-cased unless upper=False. The result is always a valid shell name, and a valid portable one when upper-cased.

from envname import sanitize_env_name

print(sanitize_env_name("my  var!!"))  # MY_VAR_
print(sanitize_env_name("1st-value"))  # _1ST_VALUE
print(sanitize_env_name("Crème brûlée"))  # CREME_BRULEE
print(sanitize_env_name("db.host", prefix="APP_"))  # APP_DB_HOST
print(sanitize_env_name("host.name", upper=False))  # host_name

Letters with no ASCII decomposition, such as ß or ø, still become an underscore.

Names that already exist

The environment can hold names no shell could type. Check names you read back with rules="environ", and names you create with shell or portable:

from envname import is_valid_env_name

env = {"PATH": "/usr/bin", "ProgramFiles(x86)": "C:/Program Files (x86)", "my-var": "1"}
print([name for name in env if not is_valid_env_name(name)])  # ['ProgramFiles(x86)', 'my-var']
print(all(is_valid_env_name(name, rules="environ") for name in env))  # True

Command line

envname check exits with status 1 when any name is invalid and explains each one on stderr; envname sanitize prints one name per argument. Put -- before a name that starts with -.

$ envname check PATH my-var
envname: invalid shell environment variable name 'my-var': '-' is not allowed at index 2
$ envname check --rules environ my-var
$ envname sanitize --prefix APP_ db.host "max connections"
APP_DB_HOST
APP_MAX_CONNECTIONS

python -m envname works the same way.

Rules

rules A valid name Source Use it for
"shell" (default) [A-Za-z_][A-Za-z0-9_]* POSIX XBD 3.216 "Name", the Bash manual names you export from a shell, reference as $NAME or ${NAME}, or interpolate in Compose files
"portable" [A-Z_][A-Z0-9_]* POSIX XBD 8.1, the names the standard utilities use names that follow the upper-case convention of the POSIX utilities, which most deployment tooling copies
"environ" not empty, no =, no NUL the C-level rules of POSIX setenv and the Windows environment documentation names you read back from os.environ or pass through to a child process

portable names are shell names, and shell names are environ names. Names are matched case-sensitively and only ASCII letters count, as in the C locale.

Things worth knowing:

  • POSIX keeps names with lower-case letters for applications, so they never clash with a variable a standard utility reads; upper-case portable names follow the utilities' own convention. POSIX advises against a leading digit in either case.
  • Windows matches names case-insensitively, and Python's os.environ upper-cases keys there. Windows uses its own case table, which disagrees with Python's str.upper for letters such as ß, ı and fi, so envname does not try to fold case for you: stick to portable names when they must work on Windows too.
  • Windows allows a leading = for the hidden per-drive variables such as =C:, while its documentation says names cannot contain =. environ follows the documentation.
  • environ checks the string, not its encoding. On POSIX, Python encodes names with os.fsencode, so a lone surrogate outside U+DC80 to U+DCFF passes the rule but cannot be set there; Windows accepts it.
  • The Windows documentation gives 32,767 characters as the largest size of a variable. Python's os.putenv on Windows refuses a name=value string longer than that, so name and value together can hold 32,766 characters, while SetEnvironmentVariableW itself stores longer names. A name on its own cannot know its value, so no rule limits length.
  • .env parsers are looser than shells: python-dotenv takes any unquoted key without =, # or whitespace. Such keys load, but a shell cannot read them, and Compose only interpolates $NAME when the name matches [_a-z][_a-z0-9]* ignoring case, which for ASCII names is exactly the shell rule set.

Why

The usual shortcuts each answer a different question:

  • str.isidentifier() checks Python identifiers, so it accepts été and file, which no shell in the C locale accepts as a name.
  • string.Template uses (?a:[_a-z][_a-z0-9]*) with re.IGNORECASE to find placeholders in a template. It is the shell rule, but it finds names in text: it does not validate one, explain a failure, or know the other two rule sets.
  • os.environ and os.putenv refuse only what the operating system refuses (an empty name, =, NUL), so my-var is stored without complaint and no shell can read it back.
  • python-dotenv takes any unquoted key without =, # or whitespace.
  • A hand-written regular expression is easy to get subtly wrong: forgetting the leading-digit rule, letting = through, accepting an empty string, or rejecting a real name such as ProgramFiles(x86) that it should only refuse to create.

envname is small, typed and has no dependencies. Its rules are tested against the POSIX and Bash texts themselves, a real bash, the real call that stores a variable on the operating system, Python's os.putenv, and the Unicode Character Database for the sanitizer.

Documentation

The guide is the full reference: every function, its errors, the command line and the stability promise. The changelog lists what changed in each release.

Development

python scripts/fetch_upstream.py downloads the specifications, the Unicode data and the upstream sources listed in tests/upstream/MANIFEST.json (they are not part of the repository; their tests skip without them). scripts/check.ps1 runs format, lint, types and tests. Set ENVNAME_E2E=1 to also build the wheel and test the installed package.

License

MIT. See LICENSE.

Metadata

Release files for envname 1.0.0

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

Source distribution (sdist)

Source distribution for envname 1.0.0
File Size Uploaded Provenance
envname-1.0.0.tar.gz 43.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for envname 1.0.0
File Interpreter ABI Platform
envname-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.4 kB

Release files / envname-1.0.0.tar.gz

Download URL envname-1.0.0.tar.gz
Size 43.3 kB
Tags Source
SHA-256 checksum
How to use checksums
94c35ff4e7b8d7ec0ed907e14d0da06611cc71c1984bdfca006a94b609d273cd
BLAKE2b-256 checksum
How to use checksums
413dc1eaea42a7453ebd85677ff7b9300f8f9336021427560b3be63f40159bf1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / envname-1.0.0-py3-none-any.whl

Download URL envname-1.0.0-py3-none-any.whl
Size 13.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7e4531edb16c494eba89dcb8894678d7c1d53bee98e4a3d54887a8642558e27d
BLAKE2b-256 checksum
How to use checksums
787ba6ff46bc5f1a227c62b0f4dc59cba0e1dc4adc3a69a110f2105e0dcd51a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Security for envname 1.0.0

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.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

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