An .env file has no universal parser. Whitespace, comments, quotes, and dollar expressions depend on the loader. Node's native parser, the npm dotenv package, Python dotenv, Compose interpolation, and Docker env-file loading are distinct mechanisms. A file accepted by one can change meaning in another.
Start with the .env guide for loading a file. Use the env validator to check our documented literal syntax, compare keys with .env.example, or generate a value-free example. It does not emulate every runtime.
What did the pinned parser comparison show?
We ran synthetic fixtures on Linux aarch64 on September 14, 2026: Node 24.21.0 using process.loadEnvFile(), npm dotenv 17.2.2, CPython 3.13.7 with python-dotenv 1.1.1, Docker Compose 2.39.4, and the Docker CLI 28.5.1 ParseEnvFile implementation. Python used its default interpolation setting. Each row was a separate file, with an isolated environment for the variables under observation.
| Fixture | Node native | npm dotenv | Python dotenv | Compose interpolation | Compose env_file | Compose raw | Docker CLI |
|---|---|---|---|---|---|---|---|
| Unterminated double quote | keep opening quote | keep opening quote | warn and skip | error | error | keep opening quote | keep opening quote |
| Double-quoted backslash-n | newline | newline | newline | newline | newline | literal + quotes | literal + quotes |
| Surrounding whitespace | trim | trim | trim | trim | trim | error | error |
| Unquoted alpha#beta | alpha | alpha | alpha#beta | alpha#beta | alpha#beta | alpha#beta | alpha#beta |
| Unquoted alpha # beta | alpha | alpha | alpha | alpha | alpha | alpha # beta | alpha # beta |
| Quoted values | remove quotes | remove quotes | remove quotes | remove quotes | remove quotes | keep quotes | keep quotes |
| Quoted physical newline | accept | accept | accept | accept | accept | error | error |
| CRLF and empty values | accept | accept | accept | accept | accept | accept | accept |
| Duplicate key | last wins | last wins | last wins | last wins | last wins | last wins | both entries¹ |
| MALFORMED LINE then VALID=after | skip malformed | skip malformed | warn and skip | error | error | error | error |
| export KEY=value | accept | accept | accept | accept | accept | error | error |
| Dollar reference | literal | literal | expand | expand | expand | literal | literal |
¹ Docker CLI returns an ordered list at this parser stage; both duplicate entries remain. This is not a container-runtime precedence observation. Compose results came from config output, without a Docker daemon. Compose escapes literal dollars as $$ when serializing config.
These results describe the exact fixtures below, not every malformed key or quoting edge case. For complete inputs, raw outputs, pinned setup, and commands, see the reproducible compatibility record. Newer versions may behave differently.
# These are separate fixtures, not one combined file.
SPACE = spaced value
HASH_JOINED=alpha#beta
HASH_SPACED=alpha # beta
HASH_QUOTED="alpha # beta"
DOUBLE="double value"
SINGLE='single value'
MULTI="line one
line two"
EMPTY=
DUP=first
DUP=second
MALFORMED LINE
VALID=after
export EXPORTED=exported
BASE=base
DOLLAR=${BASE}-suffixHow can I reproduce the comment difference?
In a disposable directory, create this synthetic file. It contains no credentials.
printf 'HASH_JOINED=alpha#beta\nHASH_SPACED=alpha # beta\nHASH_QUOTED="alpha # beta"\n' > parser-example.env
node --env-file=parser-example.env -p 'JSON.stringify([process.env.HASH_JOINED, process.env.HASH_SPACED, process.env.HASH_QUOTED])'
# Node 24.21.0 expected: ["alpha","alpha","alpha # beta"]
uv run --python 3.13.7 --with python-dotenv==1.1.1 python -c 'import json; from dotenv import dotenv_values; d=dotenv_values("parser-example.env"); print(json.dumps(list(d.values())))'
# Expected: ["alpha#beta", "alpha", "alpha # beta"]Node and npm dotenv treat any unquoted # as a comment. Python dotenv and Compose preserve a joined # but strip a whitespace-prefixed comment. Quoting a hash preserves it in these loaders; raw Docker mechanisms also preserve the quote characters themselves.
Are spaces, lowercase keys, and empty values valid?
Node, npm dotenv, Python dotenv, and Compose's default parser trimmed the surrounding whitespace in SPACE = spaced value . Shell assignment syntax is different: sourcing that line attempts to run a command. Use SPACE='spaced value' for a shell assignment.
Uppercase names are a convention. Our validator accepts names matching [A-Za-z_][A-Za-z0-9_]* , including lowercase. An assignment such as EMPTY=explicitly sets an empty string; it is not a syntax error. A missing key is a different condition.
CRLF line endings were accepted in every tested mechanism. Converting Windows line endings is not a universal fix for a variable that failed to load. Check the working directory, selected file, and loader first.
Do quotes enable expansion or escape sequences?
Quotes alone do not decide whether expansion happens. Native Node and npm dotenv left the dollar reference literal in our fixture; Python dotenv expanded it by default, and Compose expanded it. Python's interpolate=False option kept it literal. Node's native environment-file documentation does not define shell evaluation, and npm dotenv needs a separate expansion mechanism.
Do not assume a single-quoted value disables interpolation in every library. Escape handling also varies by loader. In the measured escaped fixture, double-quoted backslash-n became a newline in Node, npm dotenv, Python, and Compose default parsing. Single-quoted and unquoted backslash-n stayed literal. Raw Compose and Docker CLI kept both the backslashes and surrounding quotes. A backslash followed by n is two source characters, whereas a physical newline is a line break in the file. Use the versioned evidence and your actual loader before converting secrets between representations.
The validator deliberately performs no expansion, escape decoding, command substitution, or shell execution. It keeps dollars and backslashes literal. It rejects unsupported embedded matching quotes, malformed closing quotes, NUL characters, and backtick quoting. A successful check validates that syntax scope; it does not certify equivalent values in Node, Python, Docker, or a shell.
Can a value span multiple physical lines?
Yes in the quoted multiline fixture for Node, npm dotenv, Python dotenv, Compose interpolation, and default Compose service env_file. It failed in Compose raw mode and the Docker CLI parser. Do not transfer the default Compose result to docker run --env-file.
MESSAGE="line one
line two"The accepting parsers produced a value represented in JSON as "line one\nline two". For binary data or secret material, use your platform's supported secret-file or binding mechanism when that avoids encoding ambiguity.
How do Docker's three env-file paths differ?
docker compose --env-file file.env configsupplies values for interpolation of the Compose model.- A service's
env_file:supplies container variables using Compose's default parsing rules. env_file: [{ path: file.env, format: raw }]preserves literal values, including quotes and dollars; it is not multiline quote parsing.
The separate docker run --env-file path uses the Docker CLI parser. In our pinned observation it preserved quotes and comment suffixes. See the Compose environment guide for interpolation versus injection precedence.
What should happen when input is malformed or duplicated?
Some libraries skip malformed lines and return the remaining entries. That can make an incomplete file look plausible. Our converter blocks output on parse errors or duplicate keys. Resolve those findings before copying or downloading a result.
The env converter preserves the literal values within its supported formats and reports unsupported target cases. The validator reports duplicate assignments separately and can compare keys against .env.example. Generated examples contain keys and empty assignments only, with no copied values or comments.