Skip to main content

python-passenv

Load environment variables from the pass password store, with the ergonomics of python-dotenv.

One pass entry is one environment variable. Folders are namespaces.

~/.password-store/
├── shared/
│   └── webhook_url.gpg      →  WEBHOOK_URL
└── project_a/
    ├── api_token.gpg         →  API_TOKEN
    └── db/
        └── dsn.gpg           →  DSN
  • No .env file on disk — secrets stay encrypted until the process asks for them.
  • Zero runtime dependencies; it shells out to the pass binary you already trust.
  • Fully typed (py.typed), Python 3.10+.

Install

uv add python-passenv     # or: pip install python-passenv

You need pass on your PATH and a store you can decrypt.

Quickstart

from passenv import load_passenv

load_passenv("project_a")   # decrypts project_a/** into os.environ, returns True

import os
os.environ["API_TOKEN"]     # → 's3cret…'

Read the values without touching the process environment:

from passenv import passenv_values

config = passenv_values("project_a")     # {'API_TOKEN': 's3cret…', 'DSN': 'postgres…'}

Read a single entry by its full path:

from passenv import get_key

get_key("project_a/api_token")           # → 's3cret…'
get_key("project_a/nope")                # → None

Coming from python-dotenv

python-dotenv python-passenv
load_dotenv() load_passenv()
load_dotenv(".env.prod") load_passenv("prod")
load_dotenv(override=True) load_passenv(override=True)
dotenv_values() passenv_values()
get_key(".env", "API_TOKEN") get_key("project_a/api_token")
set_key / unset_key not in v1 — use the pass CLI
$VAR interpolation not in v1
source: a file path source: a store namespace

Namespace semantics

A namespace is a folder path relative to the store root.

load_passenv()                          # the whole store
load_passenv("project_a")               # project_a/** only
load_passenv(["shared", "project_a"])   # merged; project_a wins on conflicts
  • Recursive. project_a/db/dsn is included in load_passenv("project_a").
  • Leaf names only. That entry becomes DSN, not DB_DSN.
  • Collisions are errors. Two entries in one namespace whose leaves map to the same variable raise KeyCollisionError — detected before anything is decrypted.
  • Layering across namespaces is fine. Later namespaces override earlier ones, and the merge is resolved before anything is written to os.environ.
  • Only *.gpg files count. Dotfiles and dot-directories (.git, .gpg-id, .extensions) are skipped.

Values

By default a variable gets the first line of the entry — the pass convention of "password on line one, metadata below":

s3cret-token-value
url: https://api.example.com
user: service-account
passenv_values("project_a")["API_TOKEN"]                 # 's3cret-token-value'
passenv_values("project_a", multiline=True)["API_TOKEN"] # the whole entry, one trailing \n removed

Values are never .strip()ed — leading and trailing spaces in a secret survive intact.

Naming

key_transform is applied to the leaf name and defaults to str.upper:

load_passenv("project_a", key_transform=lambda leaf: f"APP_{leaf.upper()}")  # APP_API_TOKEN, …

Where the store lives

In precedence order:

  1. the store_dir= argument,
  2. $PASSWORD_STORE_DIR,
  3. ~/.password-store.

When store_dir is given it is exported as PASSWORD_STORE_DIR to the pass subprocess, so a non-default store works without touching your own environment.

Errors

Exception Raised when
PassEnvError base class for everything below
PassNotFoundError the pass executable is not on PATH
PassDecryptError pass show exited non-zero (.entry, .returncode, .stderr)
NamespaceNotFoundError the namespace folder is missing and required=True
KeyCollisionError two entries map to the same variable (.key, .entries)

With required=False (the default) a missing namespace is not an error: load_passenv returns False and passenv_values returns {}.

Logging

passenv logs to the passenv logger and records keys only, never values. load_passenv(verbose=True) turns those records on even if your app has not configured logging.

API

def load_passenv(namespace=None, *, override=False, multiline=False,
                 key_transform=str.upper, required=False, verbose=False,
                 store_dir=None) -> bool: ...

def passenv_values(namespace=None, *, multiline=False, key_transform=str.upper,
                   required=False, store_dir=None) -> dict[str, str]: ...

def get_key(entry, *, multiline=False, store_dir=None) -> str | None: ...

load_passenv returns True if at least one entry was read. With override=False it never clobbers a variable that is already set.

Development

uv sync --group dev
uv run prek install           # install the pre-commit and commit-msg hooks (once per clone)
uv run ruff check . && uv run ruff format --check . && uv run pyrefly check
uv run pytest                 # unit tests, no gpg required
uv run pytest -m integration  # real pass + gpg, ephemeral key and store

The integration suite generates an unprotected GPG key into a temporary GNUPGHOME, runs pass init against a temporary store, and is skipped when pass or gpg is missing. It never touches your real store.

Git hooks

prek runs the checks in prek.toml before every commit. Because this project's whole subject is secrets, the secret-scanning hooks run first and abort the commit immediately when one of them trips:

Hook Blocks
betterleaks credentials in the staged diff, matched against betterleaks' provider rules
detect-private-key PEM/OpenSSH private-key blocks
deny-filename-pattern *.gpg, *.pem, *.key, .env*, id_rsa, .netrc, …
deny-filename-pattern (store paths) anything under a .password-store/ or .gnupg/ directory

The rest of the run is repo hygiene (trailing whitespace, line endings, TOML/YAML syntax, large files) followed by ruff check --fix, ruff format and pyrefly check, all invoked through uv run so hook and CI versions come from the same dev group.

uv run prek run --all-files   # run every hook by hand
uv run prek run betterleaks   # run just one
uv run prek auto-update       # bump pinned hook revisions

CI runs the same hooks via prek run --all-files. Since the betterleaks hook only inspects staged changes, a separate CI job scans the full history with the pinned betterleaks image.

Commit messages

Commit subjects follow Conventional Commits, because the release version is computed from them — feat: bumps the minor, fix: and perf: bump the patch, everything else releases nothing. A commit-msg hook rejects subjects that do not parse. See RELEASING.md.

Releasing

Merging to main is the release. python-semantic-release reads the commits since the last tag, bumps the version, writes the changelog, tags, cuts a GitHub Release with the sdist and wheel attached, and publishes to PyPI via Trusted Publishing — or does nothing at all, if nothing releasable landed. Details, including the dry-run and forced-bump triggers, are in RELEASING.md.

License

MIT — see LICENSE.

Metadata

Release files for python-passenv 0.1.1

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

Source distribution (sdist)

Source distribution for python-passenv 0.1.1
File Size Uploaded Provenance
python_passenv-0.1.1.tar.gz 95.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for python-passenv 0.1.1
File Interpreter ABI Platform
python_passenv-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 105.6 kB

Release files / python_passenv-0.1.1.tar.gz

Download URL python_passenv-0.1.1.tar.gz
Size 95.0 kB
Tags Source
SHA-256 checksum
How to use checksums
58443d9c80e44e4f657d0fbdc3fa80dfd5db3cef673e7a6829ef2dfd0fe62915
BLAKE2b-256 checksum
How to use checksums
a58009cc0807ac22a468a8e18b3c55df8c9b856c992ab56be484b18b1d200caa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release files / python_passenv-0.1.1-py3-none-any.whl

Download URL python_passenv-0.1.1-py3-none-any.whl
Size 10.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
811f7191426088fc4eb783d53f2129839113a4a225a24d92ad1e63aa44695c08
BLAKE2b-256 checksum
How to use checksums
84b7f907e31cd57a646a2cf7242ac2b10ed4c240cdd5a0e8da3b40ec0619668a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Security for python-passenv 0.1.1

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for python_passenv-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

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.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full 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