Skip to content

.env File Backend

When to use

EnvFileBackend is the right choice when you follow the 12-factor app methodology and want to keep configuration in a plain-text .env file that lives alongside your project. It is ideal for local development and CI/CD pipelines where secrets are supplied as environment-variable overrides, and for cases where you need a human-readable, easily-diffed config file that is excluded from version control.

Never commit .env to git

Add .env to your .gitignore immediately. A committed .env file can expose credentials to anyone with access to the repository, including past contributors and CI logs.

Text Only
# .gitignore
.env
.env.*
!.env.example

Constructor parameters

Parameter Type Default Description
path str \| Path ".env" Path to the .env file. Resolved to an absolute path at init time.
load_into_environ bool False If True, sync written keys into os.environ on every mutating operation.
encoding str "utf-8" File encoding used for reading and writing.

Path resolution

The path argument is resolved to an absolute path using Path.resolve() during __init__. This means subsequent changes to the process working directory (e.g. os.chdir()) do not affect which file the backend reads and writes.

Python
import os
from credential_bridge import EnvFileBackend

backend = EnvFileBackend(path=".env")
# backend.path is now an absolute Path, e.g. /home/user/project/.env

os.chdir("/tmp")  # Does NOT change which file backend uses
backend.list_secrets()  # Still reads /home/user/project/.env

The name parameter

The name argument has different semantics depending on the operation:

  • add_secret(name, secret)name is written as a comment header (# name) above the new keys. It acts as a human-readable group label and is not itself stored as an env-var key.
  • get_secret(name)name is either an exact env-var key (e.g. "DB_HOST") or a group label (e.g. "database"). If name matches a key directly, a single-entry dict is returned. If it matches a group comment header, all keys under that group are returned together.
  • update_secret(name, secret)name is passed for consistency but the actual keys updated are determined by the secret dict, not name.
  • delete_secret(name)name is either an exact env-var key or a group label. Passing a group label removes all keys under that group and the comment header in one operation.

File format

After calling add_secret("database", {"DB_HOST": "localhost", "DB_PORT": "5432"}), the .env file contains:

Text Only
# database
DB_HOST=localhost
DB_PORT=5432

Multiple calls append additional groups:

Python
backend.add_secret("database", {"DB_HOST": "localhost", "DB_PORT": "5432"})
backend.add_secret("API_KEY", {"API_KEY": "sk-abc123"})
Text Only
# database
DB_HOST=localhost
DB_PORT=5432

# API_KEY
API_KEY=sk-abc123

Value quoting

Values that contain spaces, tabs, newlines (\n), carriage returns (\r), #, ", ', \, $, or ` are automatically wrapped in double quotes. You do not need to quote values yourself.

Python
backend.add_secret("GREETING", {"GREETING": "hello world"})
# writes: GREETING="hello world"

backend.add_secret("PATH_VAR", {"PATH_VAR": "/usr/local/bin"})
# writes: PATH_VAR=/usr/local/bin  (no quotes needed)

CRUD operations

add_secret

Appends a comment header and one or more KEY=VALUE lines to the file. Raises EnvFileKeyExistsError if any of the keys in secret already exist in the file — use update_secret() to change existing keys.

Python
from credential_bridge import EnvFileBackend

backend = EnvFileBackend(path=".env")

# Add a group of related keys
backend.add_secret("database", {"DB_HOST": "localhost", "DB_PORT": "5432"})

# Add a single key
backend.add_secret("API_KEY", {"API_KEY": "sk-abc123"})

CLI equivalent:

Bash
cb env add database --secret DB_HOST=localhost --secret DB_PORT=5432
cb env add API_KEY --secret API_KEY=sk-abc123

get_secret

Accepts either an env-var key or a group label. Raises EnvFileNotFoundError if neither is found.

By key name — returns a single-entry dict:

Python
result = backend.get_secret("DB_HOST")
# {"DB_HOST": "localhost"}

result = backend.get_secret("API_KEY")
# {"API_KEY": "sk-abc123"}

By group label — returns all keys under the matching # label comment block:

Python
# .env contains:
# # database
# DB_HOST=localhost
# DB_PORT=5432

result = backend.get_secret("database")
# {"DB_HOST": "localhost", "DB_PORT": "5432"}

If a key happens to share its name with a group label, the key lookup takes precedence.

CLI equivalent:

Bash
cb env get DB_HOST
cb env get database          # group label lookup
cb env get API_KEY --path config/.env

update_secret

Performs a partial update — only the keys specified in secret are changed; all other lines in the file are preserved exactly as they are. Raises EnvFileNotFoundError if any key in secret is missing from the file — use add_secret() first. If the .env file does not exist, all specified keys are considered missing and EnvFileNotFoundError is raised.

Python
# Only DB_HOST is changed; DB_PORT and all other keys are untouched
backend.update_secret("DB_HOST", {"DB_HOST": "prod-db.example.com"})

CLI equivalent:

Bash
cb env update DB_HOST --secret DB_HOST=prod-db.example.com

delete_secret

Accepts either an env-var key or a group label. Raises EnvFileNotFoundError if neither is found.

By key name — removes the matching KEY=VALUE line. If that was the last key under its # group_name comment header, the header is removed too, keeping the file clean.

Python
backend.delete_secret("API_KEY")

By group label — removes all KEY=VALUE lines under the matching # label comment and the comment header itself in a single operation.

Python
# Removes # database, DB_HOST=..., and DB_PORT=... in one call
backend.delete_secret("database")

CLI equivalent:

Bash
cb env delete API_KEY --yes
cb env delete database --yes   # deletes the whole group

list_secrets

Returns a list of all environment variable keys currently defined in the file, in the order they appear.

Python
keys = backend.list_secrets()
# ["DB_HOST", "DB_PORT", "API_KEY"]

CLI equivalent:

Bash
cb env list
cb env list --path config/.env

Atomic writes

All write operations (add_secret, update_secret, delete_secret) use a two-step atomic strategy to prevent file corruption if the process is interrupted mid-write:

  1. The new content is written to a temporary file with .tmp appended to the target filename (e.g. secrets.envsecrets.env.tmp) in the same directory as the target file.
  2. os.replace() is called to atomically rename the temp file back to the original filename.

os.replace() is atomic on POSIX systems and atomic on NTFS on Windows (within the same volume), so a concurrent reader never sees a partial write.

Loading into os.environ

Set load_into_environ=True to have every mutating operation automatically sync the affected keys into os.environ:

Python
import os
from credential_bridge import EnvFileBackend

backend = EnvFileBackend(path=".env", load_into_environ=True)

backend.add_secret("PORT", {"PORT": "8080"})
print(os.environ["PORT"])  # "8080"

backend.update_secret("PORT", {"PORT": "9090"})
print(os.environ["PORT"])  # "9090"

backend.delete_secret("PORT")
# os.environ["PORT"] is now unset

get_secret() and list_secrets() are read-only and never modify os.environ.

Error handling

Python
from credential_bridge import (
    EnvFileError,
    EnvFileNotFoundError,
    EnvFileKeyExistsError,
)

try:
    backend.add_secret("DB_HOST", {"DB_HOST": "new-host"})
except EnvFileKeyExistsError:
    print("Key already exists — use update_secret() to change it")

try:
    backend.get_secret("MISSING_KEY")
except EnvFileNotFoundError:
    print("Key not found in .env file")

try:
    backend.update_secret("MISSING_KEY", {"MISSING_KEY": "value"})
except EnvFileNotFoundError:
    print("One or more keys not found — use add_secret() first")

Common errors

Exception Cause Resolution
EnvFileKeyExistsError add_secret() called when one or more keys already exist Use update_secret() to change existing keys
EnvFileNotFoundError get_secret() called with a key or group label not in the file Check key names with list_secrets(); check group labels by reading the file
EnvFileNotFoundError delete_secret() called with a key or group label not in the file Verify the name with list_secrets()
EnvFileNotFoundError update_secret() called when one or more specified keys are missing Use add_secret() to create them first

EnvFileNotFoundError and EnvFileKeyExistsError are both subclasses of EnvFileError, which is itself a subclass of BackendError.