Metadata-Version: 2.4
Name: envbool
Version: 0.4.0
Summary: A small Python library and CLI tool for coercing environment variables (and arbitrary strings) into boolean values.
Keywords: environment variables,boolean,configuration,env,coerce
Author: Kyle O'Malley
Author-email: Kyle O'Malley <j.kyle.omalley@gmail.com>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/jkomalley/envbool
Project-URL: Repository, https://github.com/jkomalley/envbool
Project-URL: Issues, https://github.com/jkomalley/envbool/issues
Project-URL: Changelog, https://github.com/jkomalley/envbool/releases
Description-Content-Type: text/markdown

<div align="center">

# envbool

**Coerce environment variables and strings into booleans — sensibly.**

[![PyPI version](https://img.shields.io/pypi/v/envbool)](https://pypi.org/project/envbool/)
[![Python versions](https://img.shields.io/pypi/pyversions/envbool)](https://pypi.org/project/envbool/)
[![License: MIT](https://img.shields.io/github/license/jkomalley/envbool)](LICENSE)
[![CI](https://github.com/jkomalley/envbool/actions/workflows/ci.yml/badge.svg)](https://github.com/jkomalley/envbool/actions/workflows/ci.yml)

</div>

---

Reading a boolean out of the environment is the kind of thing every project
reinvents, slightly differently, in slightly buggy ways:

```python
DEBUG   = os.environ.get("DEBUG",   "").lower() in ("1", "true", "yes")
VERBOSE = os.environ.get("VERBOSE", "").lower() in ("1", "true", "yes")
CACHE   = os.environ.get("CACHE",   "").lower() in ("1", "true", "yes")
```

`envbool` is that snippet, done once and done properly:

```python
from envbool import envbool

DEBUG   = envbool("DEBUG")
VERBOSE = envbool("VERBOSE")
CACHE   = envbool("CACHE")
```

## Features

- **Lenient by default, strict when you want it.** Unrecognized values quietly
  become `False`, or raise on demand to catch typos in production config.
- **Always returns `bool`.** No `None`, no surprises in your type signatures.
- **Customizable value sets.** Replace or extend the truthy/falsy words your
  environment uses.
- **Process-level defaults.** Call `set_defaults()` once at startup instead
  of threading options through every call site.
- **A CLI for shell scripts.** Exit codes map to truthiness, so it drops
  straight into `&&` / `||` chains.
- **Zero ceremony.** Zero dependencies, fully typed, Python 3.11+.

## Contents

- [Installation](#installation)
- [Usage](#usage)
- [Command-line interface](#command-line-interface)
- [API reference](#api-reference)
- [Advanced topics](#advanced-topics)
- [Contributing](#contributing)
- [License](#license)

## Installation

```bash
pip install envbool
# or
uv add envbool
```

## Usage

### The basics

`envbool` is **lenient by default**: anything not recognized as truthy returns
`False`, and unset or empty variables return the default.

```python
from envbool import envbool

DEBUG = envbool("DEBUG")                 # False if unset or empty
CACHE = envbool("CACHE", default=True)   # True if unset or empty
```

The built-in truthy values are `true`, `1`, `yes`, `on`; the falsy values are
`false`, `0`, `no`, `off`. Comparison is case-insensitive and ignores
surrounding whitespace.

### Strict mode

Pass `strict=True` to raise `InvalidBoolValueError` on anything outside the
truthy/falsy sets — ideal for failing fast on a misconfigured deployment.

```python
import sys
from envbool import envbool, InvalidBoolValueError

try:
    USE_SSL = envbool("USE_SSL", strict=True)
except InvalidBoolValueError as e:
    sys.exit(f"Bad value for USE_SSL: {e.value!r}")
```

### Custom value sets

When your environment speaks a different dialect, **extend** the defaults or
**replace** them outright:

```python
# Add to the built-in sets
FEATURE = envbool("FEATURE_FLAG", extend_truthy={"enabled", "y"})

# Replace them entirely
LOCALE = envbool("USE_METRIC", truthy={"metric"}, falsy={"imperial"})
```

### Coercing arbitrary strings

Use `to_bool` for values that don't come from the environment. It accepts the
same keyword arguments as `envbool`.

```python
from envbool import to_bool

to_bool("yes")                 # True
to_bool("0")                   # False
to_bool("maybe", strict=True)  # raises InvalidBoolValueError
```

### Process-level defaults

Set policy once at startup instead of threading `strict=`/`extend_truthy=`
through every call site:

```python
import envbool

envbool.set_defaults(strict=True, extend_truthy=["enabled"])

envbool.envbool("DEBUG")  # now raises on unrecognized values by default
```

`set_defaults()` replaces the process-level defaults **from the built-ins**,
not from whatever a previous `set_defaults()` call left in place — call it
once. Call-site arguments (`envbool("X", strict=False)`) still override
whatever `set_defaults()` configured:

```
built-in defaults  →  set_defaults()  →  function arguments / CLI flags
```

`get_defaults()` returns the active `Defaults` (a frozen dataclass: `strict`,
`warn`, `effective_truthy`, `effective_falsy`) for inspection.
`reset_defaults()` restores the built-ins — call it in a test fixture (see
[Testing code that uses envbool](#testing-code-that-uses-envbool)).

> Through 0.3.x, envbool read TOML config files (`envbool.toml`,
> `[tool.envbool]`). 0.4.0 removed them in favor of `set_defaults()` — see
> `CHANGELOG.md` for the rationale and migration note.

## Command-line interface

The `envbool` command exits `0` for truthy, `1` for falsy, and `2` on error, so
it composes naturally with shell control flow.

```console
$ export DEBUG=true
$ envbool DEBUG && echo "debug is on"
debug is on

$ echo "Verbose: $(envbool --print VERBOSE)"
Verbose: false

$ echo "yes" | envbool && echo "truthy"
truthy

$ envbool --strict ENABLE_CACHE || echo "cache is off or misconfigured"
cache is off or misconfigured
```

Input is taken from a `VAR_NAME` argument, the `--value` flag, or a stdin pipe —
in that order of priority.

```console
$ envbool --help
usage: envbool [-h] [--value TEXT] [--strict] [--warn] [--default]
               [--required] [--print] [--truthy VALUE] [--falsy VALUE]
               [--extend-truthy VALUE] [--extend-falsy VALUE]
               [VAR_NAME]

Coerce an environment variable or string to a boolean.

positional arguments:
  VAR_NAME              Environment variable name to check.

options:
  -h, --help            show this help message and exit
  --value, -v TEXT      Check a literal string instead of an env var.
  --strict, -s          Raise error on unrecognized values.
  --warn                Log a warning on unrecognized values.
  --default, -d         Default value if unset/empty (default: false).
  --required, -r        Exit 2 if VAR_NAME is not set in the environment.
  --print, -p           Print "true" or "false" instead of using exit codes.
  --truthy VALUE        Replace the truthy set with VALUE (repeatable).
  --falsy VALUE         Replace the falsy set with VALUE (repeatable).
  --extend-truthy VALUE
                        Add VALUE to the truthy set (repeatable).
  --extend-falsy VALUE  Add VALUE to the falsy set (repeatable).
```

A few rules worth knowing:

- Omitting `--strict` / `--warn` uses the built-in defaults (lenient, no
  warnings). `set_defaults()` is a library-level concern — the one-shot CLI
  process doesn't read it.
- `VAR_NAME` and `--value` are mutually exclusive.
- `--required` only applies to `VAR_NAME`; combining it with `--value` or
  giving it no `VAR_NAME` at all is a usage error.
- With no `VAR_NAME`, `--value`, or non-empty piped stdin, the CLI prints
  usage and exits `2`.

## API reference

| Symbol | Description |
| --- | --- |
| `envbool(var, **opts)` | Read an environment variable and return `bool`. |
| `to_bool(value, **opts)` | Coerce a string to `bool`. |
| `set_defaults(**opts)` | Set process-level strict/warn/truthy/falsy defaults, replacing the built-ins. |
| `get_defaults()` | Return the active `Defaults`. |
| `reset_defaults()` | Restore built-in defaults. |
| `Defaults` | Frozen dataclass: `strict`, `warn`, `effective_truthy`, `effective_falsy`. |
| `DEFAULT_TRUTHY` | `frozenset` of the built-in truthy strings. |
| `DEFAULT_FALSY` | `frozenset` of the built-in falsy strings. |
| `EnvBoolError` | Base class for every exception the library raises. |
| `InvalidBoolValueError` | Raised in strict mode for unrecognized values. Also a `ValueError`. |
| `MissingEnvVarError` | Raised by `envbool(required=True)` when the variable is unset. Also a `KeyError`. |

`envbool()` and `to_bool()` share the same keyword-only options:

| Option | Type | Default | Meaning |
| --- | --- | --- | --- |
| `default` | `bool` | `False` | Returned for unset/empty input. |
| `strict` | `bool \| None` | `None` | Raise on unrecognized values (`None` defers to `set_defaults()`). |
| `warn` | `bool \| None` | `None` | Log a warning on unrecognized values (`None` defers to `set_defaults()`). |
| `truthy` / `falsy` | `Iterable[str] \| None` | `None` | **Replace** the effective set. |
| `extend_truthy` / `extend_falsy` | `Iterable[str] \| None` | `None` | **Extend** the effective set. |

`envbool()` also accepts `required` (`bool`, default `False`): when `True`, a
variable that is unset raises `MissingEnvVarError` before `default` is applied. A
variable set to an empty string counts as present and still uses `default`.

## Advanced topics

### Exception handling

Every exception inherits from `EnvBoolError`, so a single `except EnvBoolError`
catches the whole library. Catch a specific subclass when you need its detail:

```python
from envbool import envbool, InvalidBoolValueError

try:
    result = envbool("MY_VAR", strict=True)
except InvalidBoolValueError as e:
    print(e.var)    # "MY_VAR" — env var name, or None when raised from to_bool()
    print(e.value)  # "maybe" — the normalized (stripped, lowercased) value
    print(e.truthy) # frozenset({"true", "1", "yes", "on"}) — effective truthy set
    print(e.falsy)  # frozenset({"false", "0", "no", "off"}) — effective falsy set
```

`InvalidBoolValueError` also subclasses the built-in `ValueError`, so existing
`except ValueError` handlers keep working. Its message spells out exactly what
was expected:

```
InvalidBoolValueError: Invalid boolean value for MY_VAR: 'maybe'
  Expected truthy: 1, on, true, yes
  Expected falsy:  0, false, no, off
```

### Logging

`envbool` logs through the standard `logging` module under the `"envbool"`
namespace and attaches no handlers of its own — configure it like any other
library logger:

```python
import logging

logging.getLogger("envbool").setLevel(logging.DEBUG)
logging.getLogger("envbool").addHandler(logging.StreamHandler())
```

| Level | When |
| --- | --- |
| `WARNING` | An unrecognized value fell through in lenient mode (only when `warn=True`). |
| `WARNING` | The truthy and falsy sets overlap (truthy wins). |

### The unset-vs-empty distinction

`envbool()` always returns `bool` and deliberately cannot tell an unset variable
apart from one set to the empty string — both yield `default`. Most deployment
tooling can't distinguish the two either, and a plain `bool` keeps call sites
clean. When you genuinely need the distinction, check `os.environ` yourself:

```python
import os
from envbool import envbool

if "MY_VAR" not in os.environ:
    ...  # truly unset — handle the "not configured" case
else:
    result = envbool("MY_VAR")
```

### Testing code that uses envbool

If your tests call `set_defaults()`, reset it between tests with an autouse
fixture so overrides don't leak across the suite:

```python
# conftest.py
import pytest
from envbool import reset_defaults

@pytest.fixture(autouse=True)
def _reset_envbool_defaults():
    yield
    reset_defaults()
```

## Contributing

Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for development
setup, project layout, and the conventions this repo follows.

## License

Released under the [MIT License](LICENSE).
