keyring
keyring is a thin, uniform Python front end over whatever secret store the operating system already runs: macOS Keychain, the Freedesktop Secret Service (GNOME Keyring and friends), KDE KWallet, and Windows Credential Locker. The whole core API is four functions keyed by a service name and a username: set_password, get_password, delete_password, and get_credential. It picks a backend automatically by priority, and third-party packages can register their own backends through entry points, which is how 1Password, Bitwarden, and pass integrations plug in. It also installs a keyring command so shell scripts and CI can read the same secrets. Most Python developers already have it installed because pip, twine, and poetry depend on it.
The correct choice for developer-facing tools that should not leave tokens in plaintext dotfiles, and the wrong choice for anything running without a desktop session. Judge it by whether a human is logged in on the machine, not by how nice the four-function API looks.
Use it if
- You are writing a desktop or developer CLI tool and want the user's API token in the OS keychain instead of a plaintext dotfile in their home directory
- You need the same code path on macOS, Windows, and a GNOME or KDE Linux desktop without writing three platform branches
- You want a pluggable secret source: switching the whole app to 1Password or pass is installing a backend package, not editing your code
- You want the secret reachable from both Python and the shell, since the bundled keyring command reads and writes the same entries
- You are deploying to a server, container, or CI runner. There is no logged-in desktop session, so the recommended backends are unavailable and keyring raises NoKeyringError; environment variables or a real secret manager are the correct answer there
- You need your secrets protected from other code running as the same user. The README is explicit that on macOS any Python script using the same interpreter reads keyring secrets with no password prompt, and no security analysis has been done at all for the Secret Service, KWallet, or Windows backends
- You mind the dependency footprint. On Linux it pulls SecretStorage, which pulls cryptography, so a package for reading a token drags in a compiled crypto library; everywhere it pulls jaraco.classes, jaraco.functools, and jaraco.context, and those add more-itertools plus backports.tarfile on Python before 3.12
- You are storing anything large or structured. The API is a single string per service and username pair, backends impose their own size caps, and you end up hand-rolling JSON in and out
- Your CI already hangs on pip or poetry credential prompts. That is keyring, and the fix is disabling it with PYTHON_KEYRING_BACKEND rather than adding more of it
Setup reality
pip install keyring works everywhere, but working is platform-specific. macOS and Windows are the easy cases and need nothing extra. On a Linux desktop you get SecretStorage and jeepney pulled in automatically, which means cryptography is compiled or downloaded too, and the KWallet backend additionally wants dbus-python, which the README says to install as a system package because pip compilation often fails. Headless Linux needs gnome-keyring-daemon running inside a D-Bus session and opened with a password piped to its stdin, and in Docker the README suggests --privileged to dodge permission errors. Under tox you must add DBUS_SESSION_BUS_ADDRESS to pass_env or you get a NoKeyringError that looks like a bug in your code. Run keyring diagnose to find which backend was picked and where the keyringrc.cfg config file lives, because both are non-obvious.
Patterns
Store and read a secretset-and-get-password
import keyring
keyring.set_password('my-cli', 'api-token', token)
token = keyring.get_password('my-cli', 'api-token')The first argument is a namespace you invent, not anything the OS knows about; pick something specific to your tool or you will collide with another program in the same keychain.
get_password returns None, it does not raisehandle-missing-secret
import keyring
token = keyring.get_password('my-cli', 'api-token')
if token is None:
token = prompt_and_store()A missing entry and a genuinely empty stored value both come back falsy, so compare against None explicitly if an empty secret is meaningful to you.
Delete an entry without blowing up on logout twicedelete-password
import keyring
from keyring.errors import PasswordDeleteError
try:
keyring.delete_password('my-cli', 'api-token')
except PasswordDeleteError:
pass # already goneUnlike get_password, delete_password raises when the entry does not exist, so an idempotent logout command needs this try block.
Degrade gracefully when there is no keychainsurvive-no-backend
import os
import keyring
from keyring.errors import NoKeyringError
def read_token():
env = os.environ.get('MYCLI_TOKEN')
if env:
return env
try:
return keyring.get_password('my-cli', 'api-token')
except NoKeyringError:
return NoneThis is the single most important pattern in the library. Checking the environment variable first also gives CI and container users a documented way in without touching a keychain.
Turn keyring off for CI and containersdisable-in-ci
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
pip install -r requirements.txtThis is the fix for pip, poetry, or twine hanging on a credential prompt in a build. The Null backend returns empty values so callers fall back to their own behavior; keyring --disable makes it permanent for that user.
Find out which backend is actually in useinspect-active-backend
import keyring
print(keyring.get_keyring())
# <keyring.backends.macOS.Keyring object at 0x...>From the shell, keyring diagnose prints the selected backend and the path of keyringrc.cfg, which is the fastest way to explain why a colleague's machine behaves differently from yours.
Pin the backend from codeforce-a-backend
import keyring
import keyring.backends.SecretService
keyring.set_keyring(keyring.backends.SecretService.Keyring())Auto-selection is by backend priority and can change when a user installs another keyring package; pin it only if you truly need one backend, since pinning breaks cross-platform use.
Read a username and password togetherget-credential
import keyring
cred = keyring.get_credential('my-cli', None)
if cred is not None:
print(cred.username, cred.password)Passing None for the username asks the backend for whatever credential it has for that service. Since 25.5.0 this returns None when a named username is not found instead of a partially filled object.
Keep more than one field per entrystore-structured-secret
import json
import keyring
keyring.set_password('my-cli', 'oauth', json.dumps({
'access_token': access,
'refresh_token': refresh,
'expires_at': expires_at,
}))
raw = keyring.get_password('my-cli', 'oauth')
session = json.loads(raw) if raw else {}The API only stores one string, so JSON is the usual workaround. Keep it small: backends have their own size limits and a refresh token blob is close to the ceiling on some of them.
Swap in a fake backend in testscustom-backend-for-tests
import keyring
import keyring.backend
class MemoryKeyring(keyring.backend.KeyringBackend):
priority = 1
def __init__(self):
self._data = {}
def set_password(self, service, username, password):
self._data[(service, username)] = password
def get_password(self, service, username):
return self._data.get((service, username))
def delete_password(self, service, username):
del self._data[(service, username)]
keyring.set_keyring(MemoryKeyring())A backend needs a priority attribute and those three methods; get_credential is optional. This keeps your test suite from touching the developer's real keychain, which otherwise triggers OS password prompts mid-run.
Read and write from the shellcli-usage
keyring set my-cli api-token # prompts, does not echo
TOKEN=$(keyring get my-cli api-token)
keyring del my-cli api-token
python -m keyring get my-cli api-token # same thing, exact interpreterPrefer python -m keyring in scripts: the bare keyring command may resolve to a different virtualenv than the Python you are running, and the two see different keyring configurations.
Point a backend at a non-default storebackend-properties-from-env
export KEYRING_PROPERTY_KEYCHAIN=/Users/me/Library/Keychains/ci.keychain-db
python -m keyring get my-cli api-tokenAny KEYRING_PROPERTY_NAME variable sets the lowercased property on the active backend at init; keychain works on macOS and appid on the Secret Service backend.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| keyrings.alt | PyPI | You need a file-based backend on a machine with no OS keychain and you accept the weaker guarantees |
| python-dotenv | PyPI | The target is a server or container where a per-user keychain does not exist and env files are the norm |
| hvac | PyPI | Secrets belong to the deployment rather than the user and you already run HashiCorp Vault |